3488 improve profile list (#3507)
Fixes #3488 Working with lists from a profile page and in the normal "lists view" from the drawer now use the same fragment view code. (also) RFC regarding joining different list lists 
This commit is contained in:
parent
6494247301
commit
0698333665
37 changed files with 250 additions and 380 deletions
|
|
@ -66,7 +66,7 @@ import com.keylesspalace.tusky.EditProfileActivity
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
|
||||
import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
|
||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
|
|
@ -991,7 +991,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
return true
|
||||
}
|
||||
R.id.action_add_or_remove_from_list -> {
|
||||
ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
|
||||
ListSelectionFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
|
||||
return true
|
||||
}
|
||||
R.id.action_mute_domain -> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2022 kyori19
|
||||
/* Copyright Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -16,79 +16,99 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.account.list
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.ListsActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.FragmentListsForAccountBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemAddOrRemoveFromListBinding
|
||||
import com.keylesspalace.tusky.databinding.FragmentListsListBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemListBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ListsForAccountFragment : DialogFragment(), Injectable {
|
||||
class ListSelectionFragment : DialogFragment(), Injectable {
|
||||
|
||||
interface ListSelectionListener {
|
||||
fun onListSelected(list: MastoList)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
|
||||
private val binding by viewBinding(FragmentListsForAccountBinding::bind)
|
||||
|
||||
private var _binding: FragmentListsListBinding? = null
|
||||
|
||||
// This property is only valid between onCreateDialog and onDestroyView
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val adapter = Adapter()
|
||||
|
||||
private var selectListener: ListSelectionListener? = null
|
||||
private var accountId: String? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
selectListener = context as? ListSelectionListener
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
|
||||
|
||||
viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!)
|
||||
accountId = requireArguments().getString(ARG_ACCOUNT_ID)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.apply {
|
||||
window?.setLayout(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val context = requireContext()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_lists_for_account, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.listsView.layoutManager = LinearLayoutManager(view.context)
|
||||
_binding = FragmentListsListBinding.inflate(layoutInflater)
|
||||
binding.listsView.adapter = adapter
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val dialogBuilder = AlertDialog.Builder(context)
|
||||
.setView(binding.root)
|
||||
.setTitle(R.string.select_list_title)
|
||||
.setNeutralButton(R.string.select_list_manage) { _, _ ->
|
||||
val listIntent = Intent(context, ListsActivity::class.java)
|
||||
startActivity(listIntent)
|
||||
}
|
||||
.setNegativeButton(if (accountId != null) R.string.button_done else android.R.string.cancel, null)
|
||||
|
||||
val dialog = dialogBuilder.create()
|
||||
|
||||
val showProgressBarJob = getProgressBarJob(binding.progressBar, 500)
|
||||
showProgressBarJob.start()
|
||||
|
||||
// TODO change this to a (single) LoadState like elsewhere?
|
||||
lifecycleScope.launch {
|
||||
viewModel.states.collectLatest { states ->
|
||||
binding.progressBar.hide()
|
||||
showProgressBarJob.cancel()
|
||||
if (states.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
|
||||
load()
|
||||
}
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists)
|
||||
} else {
|
||||
binding.listsView.show()
|
||||
adapter.submitList(states)
|
||||
|
|
@ -96,9 +116,11 @@ class ListsForAccountFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleScope.launch {
|
||||
viewModel.loadError.collectLatest { error ->
|
||||
Log.e(TAG, "failed to load lists", error)
|
||||
binding.progressBar.hide()
|
||||
showProgressBarJob.cancel()
|
||||
binding.listsView.hide()
|
||||
binding.messageView.apply {
|
||||
show()
|
||||
|
|
@ -107,20 +129,20 @@ class ListsForAccountFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
lifecycleScope.launch {
|
||||
viewModel.actionError.collectLatest { error ->
|
||||
when (error.type) {
|
||||
ActionError.Type.ADD -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.addAccountToList(error.listId)
|
||||
viewModel.addAccountToList(accountId!!, error.listId)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
ActionError.Type.REMOVE -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.removeAccountFromList(error.listId)
|
||||
viewModel.removeAccountFromList(accountId!!, error.listId)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
|
@ -128,18 +150,35 @@ class ListsForAccountFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
binding.doneButton.setOnClickListener {
|
||||
dismiss()
|
||||
lifecycleScope.launch {
|
||||
load()
|
||||
}
|
||||
|
||||
load()
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
|
||||
start = CoroutineStart.LAZY
|
||||
) {
|
||||
try {
|
||||
delay(delayMs)
|
||||
progressView.show()
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
progressView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
binding.progressBar.show()
|
||||
binding.listsView.hide()
|
||||
binding.messageView.hide()
|
||||
viewModel.load()
|
||||
viewModel.load(accountId)
|
||||
}
|
||||
|
||||
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
|
||||
|
|
@ -159,42 +198,55 @@ class ListsForAccountFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
|
||||
inner class Adapter :
|
||||
ListAdapter<AccountListState, BindingHolder<ItemAddOrRemoveFromListBinding>>(Differ) {
|
||||
ListAdapter<AccountListState, BindingHolder<ItemListBinding>>(Differ) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemAddOrRemoveFromListBinding> {
|
||||
val binding =
|
||||
ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
): BindingHolder<ItemListBinding> {
|
||||
return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemAddOrRemoveFromListBinding>, position: Int) {
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.binding.listNameView.text = item.list.title
|
||||
holder.binding.addButton.apply {
|
||||
visible(!item.includesAccount)
|
||||
setOnClickListener {
|
||||
viewModel.addAccountToList(item.list.id)
|
||||
holder.binding.listName.text = item.list.title
|
||||
accountId?.let { accountId ->
|
||||
holder.binding.addButton.apply {
|
||||
visible(!item.includesAccount)
|
||||
setOnClickListener {
|
||||
viewModel.addAccountToList(accountId, item.list.id)
|
||||
}
|
||||
}
|
||||
holder.binding.removeButton.apply {
|
||||
visible(item.includesAccount)
|
||||
setOnClickListener {
|
||||
viewModel.removeAccountFromList(accountId, item.list.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.binding.removeButton.apply {
|
||||
visible(item.includesAccount)
|
||||
setOnClickListener {
|
||||
viewModel.removeAccountFromList(item.list.id)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
selectListener?.onListSelected(item.list)
|
||||
|
||||
accountId?.let { accountId ->
|
||||
if (item.includesAccount) {
|
||||
viewModel.removeAccountFromList(accountId, item.list.id)
|
||||
} else {
|
||||
viewModel.addAccountToList(accountId, item.list.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ListsListFragment"
|
||||
private const val ARG_ACCOUNT_ID = "accountId"
|
||||
|
||||
fun newInstance(accountId: String): ListsForAccountFragment {
|
||||
fun newInstance(accountId: String?): ListSelectionFragment {
|
||||
val args = Bundle().apply {
|
||||
putString(ARG_ACCOUNT_ID, accountId)
|
||||
}
|
||||
return ListsForAccountFragment().apply { arguments = args }
|
||||
return ListSelectionFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,6 @@ import at.connyduck.calladapter.networkresult.runCatching
|
|||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -54,8 +52,6 @@ class ListsForAccountViewModel @Inject constructor(
|
|||
private val mastodonApi: MastodonApi
|
||||
) : ViewModel() {
|
||||
|
||||
private lateinit var accountId: String
|
||||
|
||||
private val _states = MutableSharedFlow<List<AccountListState>>(1)
|
||||
val states: SharedFlow<List<AccountListState>> = _states
|
||||
|
||||
|
|
@ -65,24 +61,21 @@ class ListsForAccountViewModel @Inject constructor(
|
|||
private val _actionError = MutableSharedFlow<ActionError>(1)
|
||||
val actionError: SharedFlow<ActionError> = _actionError
|
||||
|
||||
fun setup(accountId: String) {
|
||||
this.accountId = accountId
|
||||
}
|
||||
|
||||
fun load() {
|
||||
fun load(accountId: String?) {
|
||||
_loadError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
val (all, includes) = listOf(
|
||||
async { mastodonApi.getLists() },
|
||||
async { mastodonApi.getListsIncludesAccount(accountId) }
|
||||
).awaitAll()
|
||||
val all = mastodonApi.getLists().getOrThrow()
|
||||
var includes: List<MastoList> = emptyList()
|
||||
if (accountId != null) {
|
||||
includes = mastodonApi.getListsIncludesAccount(accountId).getOrThrow()
|
||||
}
|
||||
|
||||
_states.emit(
|
||||
all.getOrThrow().map { list ->
|
||||
all.map { listState ->
|
||||
AccountListState(
|
||||
list = list,
|
||||
includesAccount = includes.getOrThrow().any { it.id == list.id }
|
||||
list = listState,
|
||||
includesAccount = includes.any { it.id == listState.id }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -93,7 +86,9 @@ class ListsForAccountViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun addAccountToList(listId: String) {
|
||||
// TODO there is no "progress" visible for these
|
||||
|
||||
fun addAccountToList(accountId: String, listId: String) {
|
||||
_actionError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
mastodonApi.addAccountToList(listId, listOf(accountId))
|
||||
|
|
@ -114,7 +109,7 @@ class ListsForAccountViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun removeAccountFromList(listId: String) {
|
||||
fun removeAccountFromList(accountId: String, listId: String) {
|
||||
_actionError.resetReplayCache()
|
||||
viewModelScope.launch {
|
||||
mastodonApi.deleteAccountFromList(listId, listOf(accountId))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue