[WIP] EMOJI REACTION PICKER!!

Honestly I don't know why most of the things here work.

It's mostly done, but it's missing a few key things I'd like to have, but
either don't know how to do it or just didn't finish it yet.

- add the button functionality to more views (i've only added it to
  the timeline - thread, search, conversations and whatsonot don't react
  to it)
- add unicode emojis to tabs (because pleroma doesn't support custom
  emojis reactions lulz)
- set first emoji as a group name
- etc
This commit is contained in:
fruye 2022-12-04 14:44:59 +01:00
parent 09dce64a78
commit 93bef32825
7 changed files with 90 additions and 12 deletions

View File

@ -95,6 +95,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private final SparkButton reblogButton; private final SparkButton reblogButton;
private final SparkButton favouriteButton; private final SparkButton favouriteButton;
private final SparkButton bookmarkButton; private final SparkButton bookmarkButton;
private final ImageButton reactButton;
private final ImageButton moreButton; private final ImageButton moreButton;
private final ConstraintLayout mediaContainer; private final ConstraintLayout mediaContainer;
protected final MediaPreviewLayout mediaPreview; protected final MediaPreviewLayout mediaPreview;
@ -151,6 +152,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
reblogButton = itemView.findViewById(R.id.status_inset); reblogButton = itemView.findViewById(R.id.status_inset);
favouriteButton = itemView.findViewById(R.id.status_favourite); favouriteButton = itemView.findViewById(R.id.status_favourite);
bookmarkButton = itemView.findViewById(R.id.status_bookmark); bookmarkButton = itemView.findViewById(R.id.status_bookmark);
reactButton = itemView.findViewById(R.id.status_react);
moreButton = itemView.findViewById(R.id.status_more); moreButton = itemView.findViewById(R.id.status_more);
emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions); emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions);
quoteView = itemView.findViewById(R.id.status_quote_view); quoteView = itemView.findViewById(R.id.status_quote_view);
@ -199,7 +201,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviewUnloaded = new ColorDrawable(MaterialColors.getColor(itemView, R.attr.colorBackgroundAccent)); mediaPreviewUnloaded = new ColorDrawable(MaterialColors.getColor(itemView, R.attr.colorBackgroundAccent));
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton)); TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, reactButton, moreButton));
} }
protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) { protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
@ -732,6 +734,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return true; return true;
}); });
if (reactButton != null) {
reactButton.setOnClickListener(v -> {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.openReactionPicker(v, position);
}
});
}
moreButton.setOnClickListener(v -> { moreButton.setOnClickListener(v -> {
int position = getBindingAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {

View File

@ -24,6 +24,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import android.widget.PopupWindow
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -31,16 +32,14 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
@ -70,6 +69,7 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
@ -90,7 +90,8 @@ class TimelineFragment :
Injectable, Injectable,
ReselectableFragment, ReselectableFragment,
RefreshableFragment, RefreshableFragment,
MenuProvider { MenuProvider,
OnEmojiSelectedListener {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -147,6 +148,25 @@ class TimelineFragment :
/** The user's preferred reading order */ /** The user's preferred reading order */
private lateinit var readingOrder: ReadingOrder private lateinit var readingOrder: ReadingOrder
private lateinit var emojiView : View
private val emojiPicker : EmojiPicker by lazy {
emojiView = layoutInflater.inflate(R.layout.emoji_picker, null)
EmojiPicker(emojiView)
}
private val pickerDialog : PopupWindow by lazy {
PopupWindow(requireContext())
.apply {
contentView = emojiView
isFocusable = true
setOnDismissListener {
pickerCurrentStatusId = null
}
}
}
private var pickerCurrentStatusId : String? = null
private lateinit var statusDisplayOptions : StatusDisplayOptions
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -178,7 +198,7 @@ class TimelineFragment :
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null)) readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null))
val statusDisplayOptions = StatusDisplayOptions( statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
@ -277,6 +297,12 @@ class TimelineFragment :
} }
} }
viewLifecycleOwner.lifecycleScope.launch {
viewModel.emoji.collectLatest {
emojiPicker.populateEmojis(it, this@TimelineFragment, statusDisplayOptions.animateEmojis)
}
}
if (actionButtonPresent()) { if (actionButtonPresent()) {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
hideFab = preferences.getBoolean("fabHide", false) hideFab = preferences.getBoolean("fabHide", false)
@ -460,6 +486,17 @@ class TimelineFragment :
(activity as BaseActivity).startActivityWithSlideInAnimation(intent) (activity as BaseActivity).startActivityWithSlideInAnimation(intent)
} }
override fun openReactionPicker(target: View, position: Int) {
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
pickerCurrentStatusId = statusId
pickerDialog.showAsDropDown(target)
}
override fun onEmojiSelected(shortcode: String) {
onEmojiReact(pickerCurrentStatusId!!, shortcode, true)
pickerDialog.dismiss()
}
override fun onShowFavs(position: Int) { override fun onShowFavs(position: Int) {
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)

View File

@ -438,7 +438,7 @@ abstract class SFragment : Fragment(), Injectable {
.show() .show()
} }
open fun onEmojiReact(id: String, emoji: String, react: Boolean) { fun onEmojiReact(id: String, emoji: String, react: Boolean) {
lifecycleScope.launch { lifecycleScope.launch {
val result = timelineCases.react(id, emoji, react).exceptionOrNull() val result = timelineCases.react(id, emoji, react).exceptionOrNull()
if (result != null) { if (result != null) {

View File

@ -72,4 +72,6 @@ public interface StatusActionListener extends LinkListener {
void clearWarningAction(int position); void clearWarningAction(int position);
default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, String statusId) {} default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, String statusId) {}
default void openReactionPicker(View target, int position) {}
} }

View File

@ -410,7 +410,7 @@
android:contentDescription="@string/action_favourite" android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_bookmark" app:layout_constraintEnd_toStartOf="@id/status_react"
app:layout_constraintStart_toEndOf="@id/status_inset" app:layout_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" sparkbutton:activeImage="@drawable/ic_favourite_active_24dp"
@ -430,6 +430,20 @@
app:layout_constraintTop_toTopOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset"
tools:text="" /> tools:text="" />
<ImageButton
android:id="@+id/status_react"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:contentDescription="@string/action_react"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toStartOf="@id/status_bookmark"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_emoji_24dp" />
<at.connyduck.sparkbutton.SparkButton <at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark" android:id="@+id/status_bookmark"
android:layout_width="52dp" android:layout_width="52dp"
@ -439,7 +453,7 @@
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more" app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite" app:layout_constraintStart_toEndOf="@id/status_react"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp" sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp"
sparkbutton:iconSize="28dp" sparkbutton:iconSize="28dp"

View File

@ -425,7 +425,7 @@
android:contentDescription="@string/action_favourite" android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_bookmark" app:layout_constraintEnd_toStartOf="@id/status_react"
app:layout_constraintStart_toEndOf="@id/status_inset" app:layout_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset" app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" sparkbutton:activeImage="@drawable/ic_favourite_active_24dp"
@ -434,6 +434,19 @@
sparkbutton:primaryColor="@color/tusky_orange" sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" /> sparkbutton:secondaryColor="@color/tusky_orange_light" />
<ImageButton
android:id="@+id/status_react"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:contentDescription="@string/action_react"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_bookmark"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_favourite"
app:srcCompat="@drawable/ic_emoji_24dp" />
<at.connyduck.sparkbutton.SparkButton <at.connyduck.sparkbutton.SparkButton
android:id="@+id/status_bookmark" android:id="@+id/status_bookmark"
android:layout_width="52dp" android:layout_width="52dp"
@ -443,7 +456,7 @@
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:padding="4dp" android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more" app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite" app:layout_constraintStart_toEndOf="@id/status_react"
app:layout_constraintTop_toTopOf="@id/status_reply" app:layout_constraintTop_toTopOf="@id/status_reply"
sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp" sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp"
sparkbutton:iconSize="28dp" sparkbutton:iconSize="28dp"

View File

@ -13,6 +13,7 @@
<string name="admin">Admin</string> <string name="admin">Admin</string>
<string name="moderator">Moderator</string> <string name="moderator">Moderator</string>
<string name="action_react">React</string>
<string name="action_emoji_react">Add reaction</string> <string name="action_emoji_react">Add reaction</string>
<string name="action_emoji_unreact">Remove reaction</string> <string name="action_emoji_unreact">Remove reaction</string>
<string name="action_emoji_reacted_by">Who reacted</string> <string name="action_emoji_reacted_by">Who reacted</string>