emoji_reactions: general refactoring, implement emoji_reactions_by

This commit is contained in:
Alibek Omarov 2020-03-06 21:04:14 +03:00
parent 0257cd3b07
commit 97ffa14268
12 changed files with 254 additions and 93 deletions

View File

@ -37,7 +37,8 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
MUTES,
FOLLOW_REQUESTS,
REBLOGGED,
FAVOURITED
FAVOURITED,
REACTED
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -46,6 +47,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
val type = intent.getSerializableExtra(EXTRA_TYPE) as Type
val id: String? = intent.getStringExtra(EXTRA_ID)
val emoji: String? = intent.getStringExtra(EXTRA_EMOJI)
setSupportActionBar(toolbar)
supportActionBar?.apply {
@ -57,6 +59,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
Type.FOLLOWS -> setTitle(R.string.title_follows)
Type.REBLOGGED -> setTitle(R.string.title_reblogged_by)
Type.FAVOURITED -> setTitle(R.string.title_favourited_by)
Type.REACTED -> setTitle(R.string.title_emoji_reacted_by)
}
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
@ -64,7 +67,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id))
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, emoji))
.commit()
}
@ -83,13 +86,20 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
companion object {
private const val EXTRA_TYPE = "type"
private const val EXTRA_ID = "id"
private const val EXTRA_EMOJI = "emoji"
@JvmStatic
fun newIntent(context: Context, type: Type, id: String? = null): Intent {
fun newIntent(context: Context, type: Type, id: String?, emoji: String?): Intent {
return Intent(context, AccountListActivity::class.java).apply {
putExtra(EXTRA_TYPE, type)
putExtra(EXTRA_ID, id)
putExtra(EXTRA_EMOJI, emoji)
}
}
@JvmStatic
fun newIntent(context: Context, type: Type, id: String? = null): Intent {
return newIntent(context, type, id, null)
}
}
}

View File

@ -0,0 +1,39 @@
package com.keylesspalace.tusky.adapter;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.emoji.widget.EmojiAppCompatButton;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
import java.util.List;
import java.util.Date;
public class EmojiReactionViewHolder extends RecyclerView.ViewHolder {
public EmojiAppCompatButton emojiReaction;
EmojiReactionViewHolder(View view) {
super(view);
emojiReaction = view.findViewById(R.id.status_emoji_reaction);
}
}

View File

@ -0,0 +1,70 @@
package com.keylesspalace.tusky.adapter;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.emoji.widget.EmojiAppCompatButton;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
import java.util.List;
import java.util.Date;
public class EmojiReactionsAdapter extends RecyclerView.Adapter<EmojiReactionViewHolder> {
private final List<EmojiReaction> reactions;
private final StatusActionListener listener;
private final String statusId;
EmojiReactionsAdapter(final List<EmojiReaction> reactions, final StatusActionListener listener, final String statusId) {
this.reactions = reactions;
this.listener = listener;
this.statusId = statusId;
}
@Override
public EmojiReactionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_emoji_reaction, parent, false);
return new EmojiReactionViewHolder(view);
}
@Override
public void onBindViewHolder(EmojiReactionViewHolder holder, int position) {
EmojiReaction reaction = reactions.get(position);
String str = reaction.getName() + " " + reaction.getCount();
// no custom emoji yet!
holder.emojiReaction.setText(str);
holder.emojiReaction.setActivated(reaction.getMe());
holder.emojiReaction.setOnClickListener(v -> {
listener.onEmojiReactMenu(v, reaction, statusId, position);
});
}
// total number of rows
@Override
public int getItemCount() {
return reactions.size();
}
}

View File

@ -28,6 +28,7 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.google.android.material.button.MaterialButton;
import com.google.android.flexbox.FlexboxLayoutManager;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus;
@ -35,6 +36,7 @@ import com.keylesspalace.tusky.entity.Attachment.MetaData;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
@ -112,6 +114,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private int avatarRadius24dp;
private final Drawable mediaPreviewUnloaded;
private RecyclerView emojiReactionsView;
protected StatusBaseViewHolder(View itemView) {
super(itemView);
@ -125,7 +129,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
favouriteButton = itemView.findViewById(R.id.status_favourite);
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
moreButton = itemView.findViewById(R.id.status_more);
emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions);
float INCREASE_HORIZONTAL_HIT_AREA = 20.0f;
ViewExtensionsKt.increaseHitArea(replyButton, 0.0f, INCREASE_HORIZONTAL_HIT_AREA);
@ -700,6 +705,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
})
.show();
}
private void setEmojiReactions(@Nullable List<EmojiReaction> reactions, final StatusActionListener listener, final String statusId) {
if(emojiReactionsView != null && reactions != null && reactions.size() > 0) {
emojiReactionsView.setVisibility(View.VISIBLE);
FlexboxLayoutManager lm = new FlexboxLayoutManager(emojiReactionsView.getContext());
emojiReactionsView.setLayoutManager(lm);
emojiReactionsView.setAdapter(new EmojiReactionsAdapter(reactions, listener, statusId));
}
}
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions) {
@ -752,6 +766,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener);
setDescriptionForStatus(status, statusDisplayOptions);
setEmojiReactions(status.getEmojiReactions(), listener, status.getId());
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
// RecyclerView tries to set AccessibilityDelegateCompat to null

View File

@ -33,14 +33,12 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView reblogs;
private TextView favourites;
private View infoDivider;
private RecyclerView emojiReactionsView;
StatusDetailedViewHolder(View view) {
super(view);
reblogs = view.findViewById(R.id.status_reblogs);
favourites = view.findViewById(R.id.status_favourites);
infoDivider = view.findViewById(R.id.status_info_divider);
emojiReactionsView = view.findViewById(R.id.status_emoji_reactions);
}
@Override
@ -108,60 +106,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
}
private class EmojiReactionViewHolder extends RecyclerView.ViewHolder {
public EmojiAppCompatButton emojiReaction;
EmojiReactionViewHolder(View view) {
super(view);
emojiReaction = view.findViewById(R.id.status_emoji_reaction);
}
}
private class EmojiReactionsAdapter extends RecyclerView.Adapter<EmojiReactionViewHolder> {
private List<EmojiReaction> reactions;
EmojiReactionsAdapter(List<EmojiReaction> reactions) {
this.reactions = reactions;
}
@Override
public EmojiReactionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_emoji_reaction, parent, false);
return new EmojiReactionViewHolder(view);
}
@Override
public void onBindViewHolder(EmojiReactionViewHolder holder, int position) {
EmojiReaction reaction = reactions.get(position);
String str = reaction.getName() + " " + reaction.getCount();
// no custom emoji yet!
holder.emojiReaction.setText(str);
holder.emojiReaction.setActivated(reaction.getMe());
holder.emojiReaction.setOnClickListener(v -> {});
}
// total number of rows
@Override
public int getItemCount() {
return reactions.size();
}
}
private void setEmojiReactions(@Nullable List<EmojiReaction> reactions) {
if(reactions != null) {
emojiReactionsView.setVisibility(View.VISIBLE);
FlexboxLayoutManager lm = new FlexboxLayoutManager(emojiReactionsView.getContext());
// lm.setFlexDirection(FlexDirection.COLUMN);
// StaggeredGridLayoutManager.HORIZONTAL);
// lm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
emojiReactionsView.setLayoutManager(lm);
emojiReactionsView.setAdapter(new EmojiReactionsAdapter(reactions));
//emojiReactionsView.setLayoutManager StaggeredGridLayoutManager
}
}
@Override
protected void setupWithStatus(final StatusViewData.Concrete status,
final StatusActionListener listener,
@ -173,7 +117,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
setApplication(status.getApplication());
setEmojiReactions(status.getEmojiReactions());
View.OnLongClickListener longClickListener = view -> {
TextView textView = (TextView) view;
ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);

View File

@ -31,8 +31,7 @@ import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.*
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
@ -58,6 +57,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
private lateinit var type: Type
private var id: String? = null
private var emojiReaction: String? = null
private lateinit var scrollListener: EndlessOnScrollListener
private lateinit var adapter: AccountAdapter
@ -68,6 +68,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
super.onCreate(savedInstanceState)
type = arguments?.getSerializable(ARG_TYPE) as Type
id = arguments?.getString(ARG_ID)
emojiReaction = arguments?.getString(ARG_EMOJI)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -273,11 +274,22 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
val statusId = requireId(type, id)
api.statusFavouritedBy(statusId, fromId)
}
Type.REACTED -> {
// HACKHACK: make compiler happy
val statusId = requireId(type, id)
api.statusFavouritedBy(statusId, fromId)
}
}
}
private fun requireId(type: Type, id: String?): String {
return requireNotNull(id) { "id must not be null for type "+type.name }
private fun requireId(type: Type, id: String?, name: String = "id"): String {
return requireNotNull(id) { name+" must not be null for type "+type.name }
}
private fun getEmojiReactionFetchCall(): Single<Response<List<EmojiReaction>>> {
val statusId = requireId(type, id)
val emoji = requireId(type, emojiReaction, "emoji")
return api.statusReactedBy(statusId, emoji)
}
private fun fetchAccounts(fromId: String? = null) {
@ -289,8 +301,25 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
if (fromId != null) {
recyclerView.post { adapter.setBottomLoading(true) }
}
if(type == Type.REACTED) {
getEmojiReactionFetchCall()
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ response ->
val emojiReaction = response.body()
getFetchCallByListType(fromId)
if (response.isSuccessful && emojiReaction != null && emojiReaction.size > 0 && emojiReaction.get(0).accounts != null) {
val linkHeader = response.headers()["Link"]
onFetchAccountsSuccess(emojiReaction.get(0).accounts!!, linkHeader)
} else {
onFetchAccountsFailure(Exception(response.message()))
}
}, {throwable ->
onFetchAccountsFailure(throwable)
})
} else {
getFetchCallByListType(fromId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ response ->
@ -305,7 +334,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
}, {throwable ->
onFetchAccountsFailure(throwable)
})
}
}
private fun onFetchAccountsSuccess(accounts: List<Account>, linkHeader: String?) {
@ -361,12 +390,14 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
private const val TAG = "AccountList" // logging tag
private const val ARG_TYPE = "type"
private const val ARG_ID = "id"
private const val ARG_EMOJI = "emoji"
fun newInstance(type: Type, id: String? = null): AccountListFragment {
fun newInstance(type: Type, id: String? = null, emoji: String? = null): AccountListFragment {
return AccountListFragment().apply {
arguments = Bundle(2).apply {
arguments = Bundle(3).apply {
putSerializable(ARG_TYPE, type)
putString(ARG_ID, id)
putString(ARG_EMOJI, emoji)
}
}
}

View File

@ -43,6 +43,7 @@ import androidx.lifecycle.Lifecycle;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.AccountListActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
import com.keylesspalace.tusky.ViewMediaActivity;
@ -57,6 +58,7 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.PollOption;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
@ -172,6 +174,33 @@ public abstract class SFragment extends BaseFragment implements Injectable {
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
getActivity().startActivity(intent);
}
protected void emojiReactMenu(@NonNull final String statusId, @NonNull final EmojiReaction reaction, View view, final int position) {
PopupMenu popup = new PopupMenu(getContext(), view);
popup.inflate(R.menu.emoji_reaction_more);
Menu menu = popup.getMenu();
menu.findItem(R.id.emoji_react).setVisible(!reaction.getMe());
menu.findItem(R.id.emoji_unreact).setVisible(reaction.getMe());
popup.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.emoji_react:
// TODO
return true;
case R.id.emoji_unreact:
// TODO
return true;
case R.id.emoji_reacted_by:
Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REACTED, statusId, reaction.getName());
((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent);
return true;
}
return false;
});
popup.show();
}
protected void more(@NonNull final Status status, View view, final int position) {
final String id = status.getActionableId();

View File

@ -52,10 +52,7 @@ import com.keylesspalace.tusky.appstore.ReblogEvent;
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.entity.*;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.CardViewMode;
@ -747,4 +744,9 @@ public final class ViewThreadFragment extends SFragment implements
protected void refreshAfterApplyingFilters() {
onRefresh();
}
@Override
public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId, final int position) {
super.emojiReactMenu(statusId, emoji, view, position);
}
}

View File

@ -18,6 +18,7 @@ package com.keylesspalace.tusky.interfaces;
import android.view.View;
import java.util.List;
import com.keylesspalace.tusky.entity.EmojiReaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -64,6 +65,7 @@ public interface StatusActionListener extends LinkListener {
void onVoteInPoll(int position, @NonNull List<Integer> choices);
default void onMute(int position, boolean isMuted) {}
default void onEmojiReact(final boolean react, final String emoji, final int position) {};
// default void onEmojiReact(final boolean react, final String emoji, final int position) {};
default void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId, final int position) {};
}

View File

@ -588,19 +588,20 @@ interface MastodonApi {
fun getNodeinfo(@Url url: String) : Single<NodeInfo>
@PUT("api/v1/pleroma/statuses/{id}/reactions/{emoji}")
fun reactWithEmoji(
@Path("id") statusId: String,
@Path("emoji") emoji: String
): Single<Status>
@DELETE("api/v1/pleroma/statuses/{id}/reactions/{emoji}")
fun unreactWithEmoji(
@Path("id") statusId: String,
@Path("emoji") emoji: String
): Single<Status>
@GET("api/v1/pleroma/statuses/{id}/reactions")
fun statusReactedBy(
@Path("id") statusId: String
): Single<List<EmojiReaction>>
fun reactWithEmoji(
@Path("id") statusId: String,
@Path("emoji") emoji: String
): Single<Status>
@DELETE("api/v1/pleroma/statuses/{id}/reactions/{emoji}")
fun unreactWithEmoji(
@Path("id") statusId: String,
@Path("emoji") emoji: String
): Single<Status>
@GET("api/v1/pleroma/statuses/{id}/reactions/{emoji}")
fun statusReactedBy(
@Path("id") statusId: String,
@Path("emoji") emoji: String
): Single<Response<List<EmojiReaction>>>
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/emoji_react"
android:title="@string/action_emoji_react" />
<item
android:id="@+id/emoji_unreact"
android:title="@string/action_emoji_unreact" />
<item
android:id="@+id/emoji_reacted_by"
android:title="@string/action_emoji_reacted_by" />
</menu>

View File

@ -2,8 +2,13 @@
<string name="action_reply_to">Reply to</string>
<string name="action_markdown">Markdown</string>
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="action_emoji_react">Add reaction</string>
<string name="action_emoji_unreact">Remove reaction</string>
<string name="action_emoji_reacted_by">Who reacted</string>
<string name="title_emoji_reacted_by">Emoji reacted by</string>
<string name="hint_appname">Application name</string>
<string name="hint_website">Application website</string>
@ -13,7 +18,7 @@
<string name="error_media_upload_size">File size exceeds instance limits</string>
<string name="notification_emoji_format">%s reacted to your post with %s</string>
<string name="notification_emoji_format">%s reacted with %s to your post</string>
<string name="notification_emoji_name">Emoji Reactions</string>
<string name="notification_emoji_description">Notifications about new emoji reactions</string>