Add "Tap to remove" option for emoji in ReactionsBottomSheet.

This commit is contained in:
Sagar
2025-03-25 20:06:19 +05:30
committed by Cody Henthorne
parent bcc11b9fbc
commit e88db06c8b
10 changed files with 185 additions and 61 deletions

View File

@@ -20,7 +20,12 @@ import java.util.List;
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
private List<ReactionDetails> data = Collections.emptyList();
private ReactionViewPagerAdapter.EventListener listener = null;
private List<ReactionDetails> data = Collections.emptyList();
void setListener(ReactionViewPagerAdapter.EventListener listener) {
this.listener = listener;
}
public void updateData(List<ReactionDetails> newData) {
data = newData;
@@ -37,7 +42,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(data.get(position));
holder.bind(data.get(position), listener);
}
@Override
@@ -51,17 +56,19 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
private final BadgeImageView badge;
private final TextView recipient;
private final TextView emoji;
private final TextView tapToRemoveText;
public ViewHolder(@NonNull View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.reactions_bottom_view_recipient_avatar);
badge = itemView.findViewById(R.id.reactions_bottom_view_recipient_badge);
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
emoji = itemView.findViewById(R.id.reactions_bottom_view_recipient_emoji);
avatar = itemView.findViewById(R.id.reactions_bottom_view_recipient_avatar);
badge = itemView.findViewById(R.id.reactions_bottom_view_recipient_badge);
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
emoji = itemView.findViewById(R.id.reactions_bottom_view_recipient_emoji);
tapToRemoveText = itemView.findViewById(R.id.reactions_bottom_view_recipient_tap_to_remove_action_text);
}
void bind(@NonNull ReactionDetails reaction) {
void bind(@NonNull ReactionDetails reaction, ReactionViewPagerAdapter.EventListener listener) {
this.emoji.setText(reaction.getDisplayEmoji());
if (reaction.getSender().isSelf()) {
@@ -69,10 +76,14 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
this.avatar.setAvatar(Glide.with(avatar), null, false);
this.badge.setBadge(null);
AvatarUtil.loadIconIntoImageView(reaction.getSender(), avatar);
itemView.setOnClickListener((view) -> listener.onClick());
tapToRemoveText.setVisibility(View.VISIBLE);
} else {
this.recipient.setText(reaction.getSender().getDisplayName(itemView.getContext()));
this.avatar.setAvatar(Glide.with(avatar), reaction.getSender(), false);
this.badge.setBadgeFromRecipient(reaction.getSender());
itemView.setOnClickListener(null);
tapToRemoveText.setVisibility(View.GONE);
}
}
}

View File

@@ -18,10 +18,12 @@ import java.util.List;
*/
class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPagerAdapter.ViewHolder> {
private int selectedPosition = 0;
private int selectedPosition = 0;
private final EventListener listener;
protected ReactionViewPagerAdapter() {
protected ReactionViewPagerAdapter(@NonNull EventListener listener) {
super(new AlwaysChangedDiffUtil<>());
this.listener = listener;
}
@NonNull EmojiCount getEmojiCount(int position) {
@@ -50,7 +52,7 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.onBind(getItem(position));
holder.onBind(getItem(position),listener);
holder.setSelected(selectedPosition);
}
@@ -80,12 +82,17 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
recycler.setAdapter(adapter);
}
public void onBind(@NonNull EmojiCount emojiCount) {
public void onBind(@NonNull EmojiCount emojiCount, EventListener listener) {
adapter.updateData(emojiCount.getReactions());
adapter.setListener(listener);
}
public void setSelected(int position) {
recycler.setNestedScrollingEnabled(getAdapterPosition() == position);
}
}
interface EventListener {
void onClick();
}
}

View File

@@ -29,15 +29,17 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
private ViewPager2 recipientPagerView;
private ReactionViewPagerAdapter recipientsAdapter;
private ReactionsViewModel viewModel;
private Callback callback;
private ViewPager2 recipientPagerView;
private ReactionViewPagerAdapter recipientsAdapter;
private ReactionsViewModel viewModel;
private Callback callback;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@@ -97,11 +99,11 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
disposables.bindTo(getViewLifecycleOwner());
setUpRecipientsRecyclerView();
setUpTabMediator(view, savedInstanceState);
MessageId messageId = new MessageId(requireArguments().getLong(ARGS_MESSAGE_ID));
setUpViewModel(messageId);
setUpRecipientsRecyclerView();
setUpTabMediator(view, savedInstanceState);
}
@Override
@@ -142,14 +144,12 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
}
private void setUpRecipientsRecyclerView() {
recipientsAdapter = new ReactionViewPagerAdapter();
recipientsAdapter = new ReactionViewPagerAdapter(() -> viewModel.removeReactionEmoji());
recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
recipientPagerView.post(() -> {
recipientsAdapter.enableNestedScrollingForPosition(position);
});
recipientPagerView.post(() -> recipientsAdapter.enableNestedScrollingForPosition(position));
}
@Override
@@ -164,7 +164,7 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
}
private void setUpViewModel(@NonNull MessageId messageId) {
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(messageId);
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(new ReactionsRepository(), messageId);
viewModel = new ViewModelProvider(this, factory).get(ReactionsViewModel.class);

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.reactions
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.ObservableEmitter
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -10,6 +11,7 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
class ReactionsRepository {
@@ -45,4 +47,15 @@ class ReactionsRepository {
)
}
}
fun sendReactionRemoval(messageId: MessageId) {
val oldReactionRecord = SignalDatabase.reactions.getReactions(messageId).firstOrNull { it.author == Recipient.self().id } ?: return
SignalExecutors.BOUNDED.execute {
MessageSender.sendReactionRemoval(
AppDependencies.application.applicationContext,
MessageId(messageId.id),
oldReactionRecord
)
}
}
}

View File

@@ -13,6 +13,7 @@ import java.util.List;
import java.util.Map;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
public class ReactionsViewModel extends ViewModel {
@@ -20,9 +21,9 @@ public class ReactionsViewModel extends ViewModel {
private final MessageId messageId;
private final ReactionsRepository repository;
public ReactionsViewModel(@NonNull MessageId messageId) {
public ReactionsViewModel(@NonNull ReactionsRepository reactionRepository, @NonNull MessageId messageId) {
this.messageId = messageId;
this.repository = new ReactionsRepository();
this.repository = reactionRepository;
}
public @NonNull Observable<List<EmojiCount>> getEmojiCounts() {
@@ -70,17 +71,23 @@ public class ReactionsViewModel extends ViewModel {
return reactions.get(reactions.size() - 1).getDisplayEmoji();
}
void removeReactionEmoji() {
repository.sendReactionRemoval(messageId);
}
static final class Factory implements ViewModelProvider.Factory {
private final MessageId messageId;
private final ReactionsRepository reactionsRepository;
private final MessageId messageId;
Factory(@NonNull MessageId messageId) {
this.messageId = messageId;
Factory(@NonNull ReactionsRepository reactionsRepository, @NonNull MessageId messageId) {
this.reactionsRepository = reactionsRepository;
this.messageId = messageId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new ReactionsViewModel(messageId));
return modelClass.cast(new ReactionsViewModel(reactionsRepository, messageId));
}
}
}

View File

@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment;
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
@@ -34,8 +35,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.keyboard.KeyboardPageCategoryIconMappingModel;
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoriesAdapter;
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
import org.thoughtcrime.securesms.reactions.ReactionsRepository;
import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
@@ -48,7 +49,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
EmojiPageViewGridAdapter.VariationSelectorListener
{
public static final String REACTION_STORAGE_KEY = "reactions_recent_emoji";
public static final String REACTION_STORAGE_KEY = "reactions_recent_emoji";
private static final String ABOUT_STORAGE_KEY = TextSecurePreferences.RECENT_STORAGE_KEY;
private static final String ARG_MESSAGE_ID = "arg_message_id";
@@ -126,7 +127,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
public static ReactWithAnyEmojiBottomSheetDialogFragment createForCallingReactions() {
ReactWithAnyEmojiBottomSheetDialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
@@ -254,9 +255,10 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
}
private void initializeViewModel() {
Bundle args = requireArguments();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY));
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
Bundle args = requireArguments();
ReactionsRepository reactionsRepository = new ReactionsRepository();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY));
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsRepository, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
viewModel = new ViewModelProvider(this, factory).get(ReactWithAnyEmojiViewModel.class);
}
@@ -273,7 +275,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
}
@Override
public void onVariationSelectorStateChanged(boolean open) { }
public void onVariationSelectorStateChanged(boolean open) {}
public interface Callback {
void onReactWithAnyEmojiDialogDismissed();
@@ -336,9 +338,9 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
}
@Override
public void onClicked() { }
public void onClicked() {}
@Override
public void onFocusLost() { }
public void onFocusLost() {}
}
}

View File

@@ -42,10 +42,12 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
private final BehaviorSubject<EmojiSearchResult> searchResults;
private final BehaviorSubject<String> selectedKey;
private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository,
long messageId,
boolean isMms,
@NonNull EmojiSearchRepository emojiSearchRepository)
private ReactWithAnyEmojiViewModel(
@NonNull ReactionsRepository reactionsRepository,
@NonNull ReactWithAnyEmojiRepository repository,
long messageId,
boolean isMms,
@NonNull EmojiSearchRepository emojiSearchRepository)
{
this.repository = repository;
this.messageId = messageId;
@@ -54,8 +56,8 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
this.searchResults = BehaviorSubject.createDefault(new EmojiSearchResult());
this.selectedKey = BehaviorSubject.createDefault(getStartingKey());
Observable<List<ReactWithAnyEmojiPage>> emojiPages = new ReactionsRepository().getReactions(new MessageId(messageId))
.map(repository::getEmojiPageModels);
Observable<List<ReactWithAnyEmojiPage>> emojiPages = reactionsRepository.getReactions(new MessageId(messageId))
.map(repository::getEmojiPageModels);
Observable<MappingModelList> emojiList = emojiPages.map(pages -> {
MappingModelList list = new MappingModelList();
@@ -154,20 +156,22 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
static class Factory implements ViewModelProvider.Factory {
private final ReactionsRepository reactionsRepository;
private final ReactWithAnyEmojiRepository repository;
private final long messageId;
private final boolean isMms;
Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
Factory(@NonNull ReactionsRepository reactionsRepository, @NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
this.reactionsRepository = reactionsRepository;
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms, new EmojiSearchRepository(AppDependencies.getApplication())));
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsRepository, repository, messageId, isMms, new EmojiSearchRepository(AppDependencies.getApplication())));
}
}