diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 78edbd7d..127e8389 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -95,6 +95,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private final SparkButton reblogButton; private final SparkButton favouriteButton; private final SparkButton bookmarkButton; + private final ImageButton reactButton; private final ImageButton moreButton; private final ConstraintLayout mediaContainer; protected final MediaPreviewLayout mediaPreview; @@ -151,6 +152,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { reblogButton = itemView.findViewById(R.id.status_inset); favouriteButton = itemView.findViewById(R.id.status_favourite); bookmarkButton = itemView.findViewById(R.id.status_bookmark); + reactButton = itemView.findViewById(R.id.status_react); moreButton = itemView.findViewById(R.id.status_more); emojiReactionsView = itemView.findViewById(R.id.status_emoji_reactions); 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)); - 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 customEmojis, StatusDisplayOptions statusDisplayOptions) { @@ -732,6 +734,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return true; }); + if (reactButton != null) { + reactButton.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.openReactionPicker(v, position); + } + }); + } + moreButton.setOnClickListener(v -> { int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index 5b6c6a7e..1db60efb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -24,6 +24,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityManager +import android.widget.PopupWindow import androidx.core.content.ContextCompat import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle @@ -31,16 +32,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.recyclerview.widget.* import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.autoDispose import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.appstore.EventHub 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.unsafeLazy import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData import com.mikepenz.iconics.IconicsDrawable @@ -90,7 +90,8 @@ class TimelineFragment : Injectable, ReselectableFragment, RefreshableFragment, - MenuProvider { + MenuProvider, + OnEmojiSelectedListener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -147,6 +148,25 @@ class TimelineFragment : /** The user's preferred reading order */ 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?) { super.onCreate(savedInstanceState) @@ -178,7 +198,7 @@ class TimelineFragment : val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null)) - val statusDisplayOptions = StatusDisplayOptions( + statusDisplayOptions = StatusDisplayOptions( animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, 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()) { val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) hideFab = preferences.getBoolean("fabHide", false) @@ -460,6 +486,17 @@ class TimelineFragment : (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) { val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt index bde4cf68..a7bbf0f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt @@ -438,7 +438,7 @@ abstract class SFragment : Fragment(), Injectable { .show() } - open fun onEmojiReact(id: String, emoji: String, react: Boolean) { + fun onEmojiReact(id: String, emoji: String, react: Boolean) { lifecycleScope.launch { val result = timelineCases.react(id, emoji, react).exceptionOrNull() if (result != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index b6b654fa..50a5aedf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -72,4 +72,6 @@ public interface StatusActionListener extends LinkListener { void clearWarningAction(int position); default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, String statusId) {} + + default void openReactionPicker(View target, int position) {} } diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index d5872bf1..3f9a182f 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -410,7 +410,7 @@ android:contentDescription="@string/action_favourite" android:importantForAccessibility="no" 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_constraintTop_toTopOf="@id/status_inset" sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" @@ -430,6 +430,20 @@ app:layout_constraintTop_toTopOf="@id/status_inset" tools:text="" /> + + + + Admin Moderator + React Add reaction Remove reaction Who reacted