emojireactions: Add a list showing who reacted on emoji

Based on these patches by a1ba:
- 97ffa14268
- 7e10c531bc
This commit is contained in:
fruye 2022-11-09 17:50:48 +01:00
parent e17f8b8613
commit 8d0a1f0927
11 changed files with 119 additions and 26 deletions

View File

@ -770,6 +770,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
EmojiReactions.setupReactions(emojiReactionsView, reactions, (Chip chip, View view, EmojiReaction reaction) -> {
chip.setChecked(reaction.getMe());
listener.onEmojiReactMenu(view, reaction, statusId);
return null;
}, displayOptions.animateEmojis());

View File

@ -38,7 +38,8 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
MUTES,
FOLLOW_REQUESTS,
REBLOGGED,
FAVOURITED
FAVOURITED,
REACTED,
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -49,6 +50,7 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
val type = intent.getSerializableExtra(EXTRA_TYPE) as Type
val id: String? = intent.getStringExtra(EXTRA_ID)
val accountLocked: Boolean = intent.getBooleanExtra(EXTRA_ACCOUNT_LOCKED, false)
val emoji: String? = intent.getStringExtra(EXTRA_EMOJI)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply {
@ -60,13 +62,14 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
Type.FOLLOWS -> setTitle(R.string.title_follows)
Type.REBLOGGED -> setTitle(R.string.title_reblogged_by)
Type.FAVOURITED -> setTitle(R.string.title_favourited_by)
Type.REACTED -> setTitle(String.format(getString(R.string.title_emoji_reacted_by), emoji))
}
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
supportFragmentManager.commit {
replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked, emoji))
}
}
@ -76,12 +79,14 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
private const val EXTRA_TYPE = "type"
private const val EXTRA_ID = "id"
private const val EXTRA_ACCOUNT_LOCKED = "acc_locked"
private const val EXTRA_EMOJI = "emoji"
fun newIntent(context: Context, type: Type, id: String? = null, accountLocked: Boolean = false): Intent {
fun newIntent(context: Context, type: Type, id: String? = null, accountLocked: Boolean = false, emoji: String? = null): Intent {
return Intent(context, AccountListActivity::class.java).apply {
putExtra(EXTRA_TYPE, type)
putExtra(EXTRA_ID, id)
putExtra(EXTRA_ACCOUNT_LOCKED, accountLocked)
putExtra(EXTRA_EMOJI, emoji)
}
}
}

View File

@ -47,6 +47,7 @@ import com.keylesspalace.tusky.components.accountlist.adapter.MutesAdapter
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.interfaces.AccountActionListener
@ -80,6 +81,7 @@ class AccountListFragment :
private lateinit var type: Type
private var id: String? = null
private var emojiReaction: String? = null
private lateinit var scrollListener: EndlessOnScrollListener
private lateinit var adapter: AccountAdapter<*>
@ -90,6 +92,7 @@ class AccountListFragment :
super.onCreate(savedInstanceState)
type = requireArguments().getSerializable(ARG_TYPE) as Type
id = requireArguments().getString(ARG_ID)
emojiReaction = arguments?.getString(ARG_EMOJI)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -294,11 +297,21 @@ class AccountListFragment :
val statusId = requireId(type, id)
api.statusFavouritedBy(statusId, fromId)
}
Type.REACTED -> throw IllegalArgumentException("Use getEmojiReactionFetchCall() instead")
}
}
private fun requireId(type: Type, id: String?): String {
return requireNotNull(id) { "id must not be null for type " + type.name }
private suspend fun getEmojiReactionFetchCall(): Response<List<EmojiReaction>> {
val statusId = requireId(type, id)
// FIXME: akkoma returns empty lists when asked for a custom emoji.
// we must currently get the full reaction list and find later the emoji we need.
// val emoji = requireId(type, emojiReaction, "emoji")
// return api.statusReactedBy(statusId, emoji)
return api.statusReactedBy(statusId, "")
}
private fun requireId(type: Type, id: String?, name: String = "id"): String {
return requireNotNull(id) { name+" must not be null for type "+type.name }
}
private fun fetchAccounts(fromId: String? = null) {
@ -312,26 +325,58 @@ class AccountListFragment :
binding.recyclerView.post { adapter.setBottomLoading(true) }
}
viewLifecycleOwner.lifecycleScope.launch {
try {
val response = getFetchCallByListType(fromId)
if (type == Type.REACTED) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val response = getEmojiReactionFetchCall()
if (!response.isSuccessful) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
if (!response.isSuccessful) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
}
val emojiReactionList = response.body()
if (emojiReactionList == null) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
}
val emojiName = requireId(type, emojiReaction, "emoji")
val reaction = emojiReactionList.find { it.name == emojiName }
if (reaction?.accounts == null) {
onFetchAccountsFailure(Exception("Couldn't get account list for an emoji"))
return@launch
}
val linkHeader = response.headers()["Link"]
onFetchAccountsSuccess(reaction.accounts, linkHeader)
} catch (throwable: IOException) {
onFetchAccountsFailure(throwable)
}
}
} else {
viewLifecycleOwner.lifecycleScope.launch {
try {
val response = getFetchCallByListType(fromId)
val accountList = response.body()
if (!response.isSuccessful) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
}
if (accountList == null) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
val accountList = response.body()
if (accountList == null) {
onFetchAccountsFailure(Exception(response.message()))
return@launch
}
val linkHeader = response.headers()["Link"]
onFetchAccountsSuccess(accountList, linkHeader)
} catch (throwable: IOException) {
onFetchAccountsFailure(throwable)
}
val linkHeader = response.headers()["Link"]
onFetchAccountsSuccess(accountList, linkHeader)
} catch (exception: IOException) {
onFetchAccountsFailure(exception)
}
}
}
@ -405,13 +450,15 @@ class AccountListFragment :
private const val ARG_TYPE = "type"
private const val ARG_ID = "id"
private const val ARG_ACCOUNT_LOCKED = "acc_locked"
private const val ARG_EMOJI = "emoji"
fun newInstance(type: Type, id: String? = null, accountLocked: Boolean = false): AccountListFragment {
fun newInstance(type: Type, id: String? = null, accountLocked: Boolean = false, emoji: String? = null): AccountListFragment {
return AccountListFragment().apply {
arguments = Bundle(3).apply {
putSerializable(ARG_TYPE, type)
putString(ARG_ID, id)
putBoolean(ARG_ACCOUNT_LOCKED, accountLocked)
putString(ARG_EMOJI, emoji)
}
}
}

View File

@ -46,6 +46,7 @@ import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment

View File

@ -50,6 +50,7 @@ import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener

View File

@ -55,6 +55,7 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
@ -515,6 +516,10 @@ class TimelineFragment :
super.viewAccount(id)
}
override fun onEmojiReactMenu(view: View, emoji: EmojiReaction, statusId: String) {
super.emojiReactMenu(statusId, view, emoji)
}
private fun onPreferenceChanged(key: String) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
when (key) {

View File

@ -44,6 +44,7 @@ import com.keylesspalace.tusky.components.viewthread.edits.ViewEditsFragment
import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
@ -401,6 +402,10 @@ class ViewThreadFragment :
super.viewAccount(id)
}
override fun onEmojiReactMenu(view: View, emoji: EmojiReaction, statusId: String) {
super.emojiReactMenu(statusId, view, emoji)
}
public override fun removeItem(position: Int) {
adapter.currentList.getOrNull(position)?.let { status ->
if (status.isDetailed) {

View File

@ -16,11 +16,7 @@ package com.keylesspalace.tusky.fragment
import android.Manifest
import android.app.DownloadManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.*
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
@ -43,6 +39,8 @@ import com.keylesspalace.tusky.PostLookupFallbackBehavior
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity.Companion.newHashtagIntent
import com.keylesspalace.tusky.ViewMediaActivity.Companion.newIntent
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.startIntent
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
@ -51,6 +49,7 @@ import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.EmojiReaction
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.network.MastodonApi
@ -62,6 +61,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.launch
import javax.inject.Inject
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
* of that is complicated by how they're coupled with Status and Notification and the corresponding
@ -140,6 +140,22 @@ abstract class SFragment : Fragment(), Injectable {
requireActivity().startActivity(intent)
}
protected open fun emojiReactMenu(statusId: String, view: View, reaction: EmojiReaction) {
val popup = PopupMenu(requireContext(), view)
popup.inflate(R.menu.emoji_reaction_more)
popup.setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) {
R.id.emoji_reacted_by -> {
val intent = newIntent(requireContext(), AccountListActivity.Type.REACTED, statusId, false /* FIXME */, reaction.name)
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent)
return@setOnMenuItemClickListener true
}
}
false
}
popup.show()
}
protected fun more(status: Status, view: View, position: Int) {
val id = status.actionableId
val accountId = status.actionableStatus.account.id

View File

@ -22,6 +22,8 @@ import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.EmojiReaction;
public interface StatusActionListener extends LinkListener {
void onReply(int position);
void onReblog(final boolean reblog, final int position);
@ -67,4 +69,5 @@ public interface StatusActionListener extends LinkListener {
void clearWarningAction(int position);
default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, String statusId) {}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/emoji_reacted_by"
android:title="@string/action_emoji_reacted_by" />
</menu>

View File

@ -6,4 +6,7 @@
<string name="admin">Admin</string>
<string name="moderator">Moderator</string>
<string name="action_emoji_reacted_by">Who reacted</string>
<string name="title_emoji_reacted_by">%s reacted by</string>
</resources>