From 0698333665d09893861808cf0401a20975fdb6fa Mon Sep 17 00:00:00 2001 From: UlrichKu Date: Wed, 3 Jan 2024 21:17:03 +0100 Subject: [PATCH] 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 ![grafik](https://user-images.githubusercontent.com/1618905/229463168-397bd943-82d8-4e05-a8bf-9fcf22f6c1f9.png) --- .../com/keylesspalace/tusky/ListsActivity.kt | 68 +++---- .../tusky/TabPreferenceActivity.kt | 111 +++--------- .../components/account/AccountActivity.kt | 4 +- ...ntFragment.kt => ListSelectionFragment.kt} | 168 ++++++++++++------ .../account/list/ListsForAccountViewModel.kt | 31 ++-- .../tusky/di/ActivitiesModule.kt | 2 +- .../tusky/di/FragmentBuildersModule.kt | 4 +- .../res/layout/fragment_lists_for_account.xml | 56 ------ .../main/res/layout/fragment_lists_list.xml | 36 ++++ .../layout/item_add_or_remove_from_list.xml | 50 ------ app/src/main/res/layout/item_list.xml | 39 +++- app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values-be/strings.xml | 2 - app/src/main/res/values-bg/strings.xml | 3 +- app/src/main/res/values-cy/strings.xml | 5 +- app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-fa/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values-gd/strings.xml | 2 - app/src/main/res/values-gl/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-is/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-lv/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 2 - app/src/main/res/values-oc/strings.xml | 2 - app/src/main/res/values-pt-rPT/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-uk/strings.xml | 2 - app/src/main/res/values-vi/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - 37 files changed, 250 insertions(+), 380 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/components/account/list/{ListsForAccountFragment.kt => ListSelectionFragment.kt} (51%) delete mode 100644 app/src/main/res/layout/fragment_lists_for_account.xml create mode 100644 app/src/main/res/layout/fragment_lists_list.xml delete mode 100644 app/src/main/res/layout/item_add_or_remove_from_list.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 955c2bd5f..3f56c914a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -1,4 +1,4 @@ -/* Copyright 2017 Andrew Dawson +/* Copyright Tusky contributors * * This file is a part of Tusky. * @@ -23,9 +23,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton import android.widget.PopupMenu -import android.widget.TextView import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog @@ -35,14 +33,14 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.color.MaterialColors import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.databinding.DialogListBinding +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 @@ -54,18 +52,12 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial -import com.mikepenz.iconics.utils.colorInt -import com.mikepenz.iconics.utils.sizeDp import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import kotlinx.coroutines.launch import javax.inject.Inject -/** - * Created by charlag on 1/4/18. - */ +// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?) class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { @@ -214,9 +206,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { ).show() } - private fun onListSelected(listId: String, listTitle: String) { + private fun onListSelected(list: MastoList) { startActivityWithSlideInAnimation( - StatusListActivity.newListIntent(this, listId, listTitle) + StatusListActivity.newListIntent(this, list.id, list.title) ) } @@ -255,42 +247,28 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { } private inner class ListsAdapter : - ListAdapter(ListsDiffer) { + ListAdapter>(ListsDiffer) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { - return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) - .let(this::ListViewHolder) - .apply { - val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary) - val context = nameTextView.context - val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder { + return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } - nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val item = getItem(position) + holder.binding.listName.text = item.title + + holder.binding.moreButton.apply { + visible(true) + setOnClickListener { + onMore(item, holder.binding.moreButton) } - } - - override fun onBindViewHolder(holder: ListViewHolder, position: Int) { - holder.nameTextView.text = getItem(position).title - } - - private inner class ListViewHolder(view: View) : - RecyclerView.ViewHolder(view), - View.OnClickListener { - val nameTextView: TextView = view.findViewById(R.id.list_name_textview) - val moreButton: ImageButton = view.findViewById(R.id.editListButton) - - init { - view.setOnClickListener(this) - moreButton.setOnClickListener(this) } - override fun onClick(v: View) { - if (v == itemView) { - val list = getItem(bindingAdapterPosition) - onListSelected(list.id, list.title) - } else { - onMore(getItem(bindingAdapterPosition), v) - } + holder.itemView.setOnClickListener { + onListSelected(item) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index f830f397b..92a2926c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -15,18 +15,10 @@ package com.keylesspalace.tusky -import android.content.Intent import android.graphics.Color import android.os.Bundle -import android.util.Log -import android.view.Gravity import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.AppCompatEditText @@ -38,34 +30,29 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager -import at.connyduck.calladapter.networkresult.fold import at.connyduck.sparkbutton.helpers.Utils -import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.MaterialArcMotion import com.google.android.material.transition.MaterialContainerTransform import com.keylesspalace.tusky.adapter.ItemInteractionListener import com.keylesspalace.tusky.adapter.TabAdapter import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.components.account.list.ListSelectionFragment import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.getDimension -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.util.visible -import kotlinx.coroutines.CoroutineStart +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.regex.Pattern import javax.inject.Inject -class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener { +class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener { @Inject lateinit var mastodonApi: MastodonApi @@ -73,6 +60,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene @Inject lateinit var eventHub: EventHub + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + private val binding by viewBinding(ActivityTabPreferenceBinding::inflate) private lateinit var currentTabs: MutableList @@ -267,81 +257,24 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene editText.requestFocus() } + private var listSelectDialog: ListSelectionFragment? = null + private fun showSelectListDialog() { - val adapter = object : ArrayAdapter(this, android.R.layout.simple_list_item_1) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = super.getView(position, convertView, parent) - getItem(position)?.let { item -> (view as TextView).text = item.title } - return view - } - } + listSelectDialog = ListSelectionFragment.newInstance(null) + listSelectDialog?.show(supportFragmentManager, null) - val statusLayout = LinearLayout(this) - statusLayout.gravity = Gravity.CENTER - val progress = ProgressBar(this) - val preferredPadding = getDimension(this, androidx.appcompat.R.attr.dialogPreferredPadding) - progress.setPadding(preferredPadding, 0, preferredPadding, 0) - progress.visible(false) - - val noListsText = TextView(this) - noListsText.setPadding(preferredPadding, 0, preferredPadding, 0) - noListsText.text = getText(R.string.select_list_empty) - noListsText.visible(false) - - statusLayout.addView(progress) - statusLayout.addView(noListsText) - - val dialogBuilder = AlertDialog.Builder(this) - .setTitle(R.string.select_list_title) - .setNeutralButton(R.string.select_list_manage) { _, _ -> - val listIntent = Intent(applicationContext, ListsActivity::class.java) - startActivity(listIntent) - } - .setNegativeButton(android.R.string.cancel, null) - .setView(statusLayout) - .setAdapter(adapter) { _, position -> - adapter.getItem(position)?.let { item -> - val newTab = createTabDataFromId(LIST, listOf(item.id, item.title)) - currentTabs.add(newTab) - currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) - updateAvailableTabs() - saveTabs() - } - } - - val showProgressBarJob = getProgressBarJob(progress, 500) - showProgressBarJob.start() - - val dialog = dialogBuilder.show() - - lifecycleScope.launch { - mastodonApi.getLists().fold( - { lists -> - showProgressBarJob.cancel() - adapter.addAll(lists) - if (lists.isEmpty()) { - noListsText.show() - } - }, - { throwable -> - dialog.hide() - Log.e("TabPreferenceActivity", "failed to load lists", throwable) - Snackbar.make(binding.root, R.string.error_list_load, Snackbar.LENGTH_LONG).show() - } - ) - } + return } - private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch( - start = CoroutineStart.LAZY - ) { - try { - delay(delayMs) - progressView.show() - awaitCancellation() - } finally { - progressView.hide() - } + override fun onListSelected(list: MastoList) { + listSelectDialog?.dismiss() + listSelectDialog = null + + val newTab = createTabDataFromId(LIST, listOf(list.id, list.title)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() } private fun validateHashtag(input: CharSequence?): Boolean { @@ -419,6 +352,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } } + override fun androidInjector() = dispatchingAndroidInjector + companion object { private const val MIN_TAB_COUNT = 2 } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index 28056f921..ee0f88f8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -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 -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt similarity index 51% rename from app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt index 08c93756b..7871878ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt @@ -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() { @@ -159,42 +198,55 @@ class ListsForAccountFragment : DialogFragment(), Injectable { } inner class Adapter : - ListAdapter>(Differ) { + ListAdapter>(Differ) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int - ): BindingHolder { - val binding = - ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return BindingHolder(binding) + ): BindingHolder { + return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) } - override fun onBindViewHolder(holder: BindingHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, 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 } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt index 110096966..ccb57e1c8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt @@ -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>(1) val states: SharedFlow> = _states @@ -65,24 +61,21 @@ class ListsForAccountViewModel @Inject constructor( private val _actionError = MutableSharedFlow(1) val actionError: SharedFlow = _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 = 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)) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index f60c78f8a..a8fe4b9b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -100,7 +100,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesLicenseActivity(): LicenseActivity - @ContributesAndroidInjector + @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity @ContributesAndroidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt index 929c1920f..b1a8b17ad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -16,7 +16,7 @@ package com.keylesspalace.tusky.di import com.keylesspalace.tusky.AccountsInListFragment -import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment +import com.keylesspalace.tusky.components.account.list.ListSelectionFragment import com.keylesspalace.tusky.components.account.media.AccountMediaFragment import com.keylesspalace.tusky.components.accountlist.AccountListFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment @@ -97,7 +97,7 @@ abstract class FragmentBuildersModule { abstract fun preferencesFragment(): PreferencesFragment @ContributesAndroidInjector - abstract fun listsForAccountFragment(): ListsForAccountFragment + abstract fun listsForAccountFragment(): ListSelectionFragment @ContributesAndroidInjector abstract fun trendingTagsFragment(): TrendingTagsFragment diff --git a/app/src/main/res/layout/fragment_lists_for_account.xml b/app/src/main/res/layout/fragment_lists_for_account.xml deleted file mode 100644 index 796048096..000000000 --- a/app/src/main/res/layout/fragment_lists_for_account.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - -