Add ability to edit default reactions.

This commit is contained in:
Cody Henthorne
2021-05-27 11:21:11 -04:00
parent 811bef8c35
commit e5b0941d30
19 changed files with 587 additions and 37 deletions

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.reactions.any;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.KeyEvent;
@@ -35,6 +36,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -53,6 +55,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
private static final String ARG_START_PAGE = "arg_start_page";
private static final String ARG_SHADOWS = "arg_shadows";
private static final String ARG_RECENT_KEY = "arg_recent_key";
private static final String ARG_EDIT = "arg_edit";
private ReactWithAnyEmojiViewModel viewModel;
private TextSwitcher categoryLabel;
@@ -62,6 +65,8 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
private SparseArray<ReactWithAnyEmojiAdapter.ScrollableChild> pageArray = new SparseArray<>();
private Callback callback;
private ReactionsLoader reactionsLoader;
private View editReactions;
private boolean showEditReactions;
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
@@ -72,6 +77,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
args.putInt(ARG_START_PAGE, startingPage);
args.putBoolean(ARG_SHADOWS, false);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
args.putBoolean(ARG_EDIT, true);
fragment.setArguments(args);
return fragment;
@@ -91,11 +97,29 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
return fragment;
}
public static DialogFragment createForEditReactions() {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putBoolean(ARG_SHADOWS, false);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
callback = (Callback) context;
if (getParentFragment() instanceof Callback) {
callback = (Callback) getParentFragment();
} else {
callback = (Callback) context;
}
}
@Override
@@ -159,6 +183,12 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
categoryLabel = view.findViewById(R.id.category_label);
categoryPager = view.findViewById(R.id.category_pager);
editReactions = view.findViewById(R.id.edit_reactions);
showEditReactions = requireArguments().getBoolean(ARG_EDIT, false);
if (showEditReactions) {
editReactions.setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class)));
}
adapter = new ReactWithAnyEmojiAdapter(this, this, (position, pageView) -> {
pageArray.put(position, pageView);
@@ -264,6 +294,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
}
categoryLabel.setText(getString(adapter.getItem(position).getLabel()));
editReactions.setVisibility(showEditReactions && position == 0 ? View.VISIBLE : View.GONE);
}
private int getStartingPage(boolean firstPageHasContent) {

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.reactions.edit
import android.os.Bundle
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
class EditReactionsActivity : PassphraseRequiredActivity() {
private val theme: DynamicTheme = DynamicNoActionBarTheme()
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
theme.onCreate(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, EditReactionsFragment())
.commit()
}
}
override fun onResume() {
super.onResume()
theme.onResume(this)
}
}

View File

@@ -0,0 +1,176 @@
package org.thoughtcrime.securesms.reactions.edit
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.ViewModelProviders
import androidx.transition.ChangeBounds
import androidx.transition.Transition
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.animation.transitions.AlphaTransition
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.ViewUtil
private val SELECTED_SIZE = ViewUtil.dpToPx(36)
private val UNSELECTED_SIZE = ViewUtil.dpToPx(26)
/**
* Edit default reactions that show when long pressing.
*/
class EditReactionsFragment : LoggingFragment(R.layout.edit_reactions_fragment), ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
private lateinit var toolbar: Toolbar
private lateinit var reactionViews: List<EmojiImageView>
private lateinit var scrubber: ConstraintLayout
private lateinit var mask: View
private lateinit var defaultSet: ConstraintSet
private lateinit var viewModel: EditReactionsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar)
toolbar.setTitle(R.string.EditReactionsFragment__edit_reactions)
toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
reactionViews = listOf(
view.findViewById(R.id.reaction_1),
view.findViewById(R.id.reaction_2),
view.findViewById(R.id.reaction_3),
view.findViewById(R.id.reaction_4),
view.findViewById(R.id.reaction_5),
view.findViewById(R.id.reaction_6)
)
reactionViews.forEach { it.setOnClickListener(this::onEmojiClick) }
scrubber = view.findViewById(R.id.edit_reactions_fragment_scrubber)
defaultSet = ConstraintSet().apply { clone(scrubber) }
mask = view.findViewById(R.id.edit_reactions_fragment_reaction_mask)
view.findViewById<View>(R.id.edit_reactions_reset_emoji).setOnClickListener { viewModel.resetToDefaults() }
view.findViewById<View>(R.id.edit_reactions_fragment_save).setOnClickListener {
viewModel.save()
requireActivity().onBackPressed()
}
viewModel = ViewModelProviders.of(this).get(EditReactionsViewModel::class.java)
viewModel.reactions.observe(viewLifecycleOwner) { emojis ->
emojis.forEachIndexed { index, emoji -> reactionViews[index].setImageEmoji(emoji) }
}
viewModel.selection.observe(viewLifecycleOwner) { selection ->
if (selection == EditReactionsViewModel.NO_SELECTION) {
deselectAll()
ObjectAnimator.ofFloat(mask, "alpha", 0f).start()
} else {
ObjectAnimator.ofFloat(mask, "alpha", 1f).start()
select(reactionViews[selection])
ReactWithAnyEmojiBottomSheetDialogFragment.createForEditReactions().show(childFragmentManager, REACT_SHEET_TAG)
}
}
view.setOnClickListener { viewModel.setSelection(EditReactionsViewModel.NO_SELECTION) }
}
private fun select(emojiImageView: EmojiImageView) {
val set = ConstraintSet()
set.clone(scrubber)
reactionViews.forEach { view ->
view.clearAnimation()
view.rotation = 0f
if (view.id == emojiImageView.id) {
set.constrainWidth(view.id, SELECTED_SIZE)
set.constrainHeight(view.id, SELECTED_SIZE)
set.setAlpha(view.id, 1f)
} else {
set.constrainWidth(view.id, UNSELECTED_SIZE)
set.constrainHeight(view.id, UNSELECTED_SIZE)
set.setAlpha(view.id, 0.3f)
}
}
TransitionManager.beginDelayedTransition(scrubber, createSelectTransitionSet(emojiImageView))
set.applyTo(scrubber)
}
private fun deselectAll() {
reactionViews.forEach { it.clearAnimation() }
TransitionManager.beginDelayedTransition(scrubber, createTransitionSet())
defaultSet.applyTo(scrubber)
}
private fun onEmojiClick(view: View) {
viewModel.setSelection(reactionViews.indexOf(view))
}
override fun onReactWithAnyEmojiDialogDismissed() {
viewModel.setSelection(EditReactionsViewModel.NO_SELECTION)
}
override fun onReactWithAnyEmojiPageChanged(page: Int) {
}
override fun onReactWithAnyEmojiSelected(emoji: String) {
viewModel.onEmojiSelected(emoji)
}
companion object {
private const val REACT_SHEET_TAG = "REACT_SHEET_TAG"
private fun createTransitionSet(): Transition {
return TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
duration = 250
addTransition(AlphaTransition())
addTransition(ChangeBounds())
}
}
private fun createSelectTransitionSet(target: View): Transition {
return createTransitionSet().addListener(object : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {
startRockingAnimation(target)
}
override fun onTransitionStart(transition: Transition) = Unit
override fun onTransitionCancel(transition: Transition) = Unit
override fun onTransitionPause(transition: Transition) = Unit
override fun onTransitionResume(transition: Transition) = Unit
})
}
private fun startRockingAnimation(target: View) {
val startRocking: Animation = AnimationUtils.loadAnimation(target.context, R.anim.rock_start)
startRocking.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) {
val continualRocking: Animation = AnimationUtils.loadAnimation(target.context, R.anim.rock)
continualRocking.repeatCount = Animation.INFINITE
continualRocking.repeatMode = Animation.REVERSE
target.startAnimation(continualRocking)
}
override fun onAnimationStart(animation: Animation?) = Unit
override fun onAnimationRepeat(animation: Animation?) = Unit
})
target.clearAnimation()
target.startAnimation(startRocking)
}
}
}

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.reactions.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.keyvalue.EmojiValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.livedata.Store
class EditReactionsViewModel : ViewModel() {
private val emojiValues: EmojiValues = SignalStore.emojiValues()
private val store: Store<State> = Store(State(reactions = emojiValues.reactions.map { emojiValues.getPreferredVariation(it) }))
val reactions: LiveData<List<String>> = LiveDataUtil.mapDistinct(store.stateLiveData, State::reactions)
val selection: LiveData<Int> = LiveDataUtil.mapDistinct(store.stateLiveData, State::selection)
fun setSelection(selection: Int) {
store.update { it.copy(selection = selection) }
}
fun onEmojiSelected(emoji: String) {
store.update { state ->
if (state.selection != NO_SELECTION && state.selection in state.reactions.indices) {
val preferredEmoji: String = emojiValues.getPreferredVariation(emoji)
val newReactions: List<String> = state.reactions.toMutableList().apply { set(state.selection, preferredEmoji) }
state.copy(reactions = newReactions)
} else {
state
}
}
}
fun resetToDefaults() {
store.update { it.copy(reactions = EmojiValues.DEFAULT_REACTIONS_LIST) }
}
fun save() {
emojiValues.reactions = store.state.reactions
}
companion object {
const val NO_SELECTION: Int = -1
}
data class State(val selection: Int = NO_SELECTION, val reactions: List<String>)
}