mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add ability to edit default reactions.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
Reference in New Issue
Block a user