change: Use checkboxes to show whether an account is a member of a list (#1036)

User reports suggested that the "x" / "+" icons used to add / remove
accounts from lists could be difficult to visually distinguish.

Replace with checkboxes. Checked indicates the account is in the list,
unchecked indicates it isn't. Clicking the checkbox to change state
changes the presence in the list.

Fixes #812
This commit is contained in:
Nik Clayton 2024-10-22 16:57:03 +02:00 committed by GitHub
parent 2234c4c782
commit 850b702092
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 97 deletions

View File

@ -65,8 +65,8 @@ import timber.log.Timber
private typealias AccountInfo = Pair<TimelineAccount, Boolean> private typealias AccountInfo = Pair<TimelineAccount, Boolean>
/** /**
* Display the members of a given list with UI to add/remove existing accounts * Display the members of a given list with a checkbox to add/remove existing,
* and search for followers to add them to the list. * accounts and search for followed accounts to add them to the list.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class AccountsInListFragment : DialogFragment() { class AccountsInListFragment : DialogFragment() {
@ -229,11 +229,12 @@ class AccountsInListFragment : DialogFragment() {
val binding = ItemAccountInListBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemAccountInListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = BindingHolder(binding) val holder = BindingHolder(binding)
binding.addButton.hide() binding.checkBox.setOnCheckedChangeListener { _, isChecked ->
binding.removeButton.setOnClickListener { if (!isChecked) {
onRemoveFromList(getItem(holder.bindingAdapterPosition).id) onRemoveFromList(getItem(holder.bindingAdapterPosition).id)
}
} }
binding.removeButton.contentDescription = binding.checkBox.contentDescription =
binding.root.context.getString(R.string.action_remove_from_list) binding.root.context.getString(R.string.action_remove_from_list)
return holder return holder
@ -241,9 +242,10 @@ class AccountsInListFragment : DialogFragment() {
override fun onBindViewHolder(holder: BindingHolder<ItemAccountInListBinding>, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAccountInListBinding>, position: Int) {
val account = getItem(position) val account = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) holder.binding.displayName.text = account.name.emojify(account.emojis, holder.binding.displayName, animateEmojis)
holder.binding.usernameTextView.text = account.username holder.binding.username.text = binding.root.context.getString(DR.string.post_username_format, account.username)
holder.binding.avatarBadge.visible(account.bot) holder.binding.avatarBadge.visible(account.bot)
holder.binding.checkBox.isChecked = true
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
} }
} }
@ -265,13 +267,13 @@ class AccountsInListFragment : DialogFragment() {
val binding = ItemAccountInListBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemAccountInListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = BindingHolder(binding) val holder = BindingHolder(binding)
binding.addButton.hide() binding.checkBox.setOnCheckedChangeListener { buttonView, isChecked ->
binding.removeButton.setOnClickListener {
val (account, inAList) = getItem(holder.bindingAdapterPosition) val (account, inAList) = getItem(holder.bindingAdapterPosition)
if (inAList) {
onRemoveFromList(account.id) if (isChecked) {
} else {
onAddToList(account) onAddToList(account)
} else {
onRemoveFromList(account.id)
} }
} }
@ -281,26 +283,23 @@ class AccountsInListFragment : DialogFragment() {
override fun onBindViewHolder(holder: BindingHolder<ItemAccountInListBinding>, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAccountInListBinding>, position: Int) {
val (account, inAList) = getItem(position) val (account, inAList) = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) holder.binding.displayName.text = account.name.emojify(account.emojis, holder.binding.displayName, animateEmojis)
holder.binding.usernameTextView.text = account.username holder.binding.username.text = binding.root.context.getString(DR.string.post_username_format, account.username)
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
holder.binding.avatarBadge.visible(account.bot) holder.binding.avatarBadge.visible(account.bot)
holder.binding.removeButton.apply { with(holder.binding.checkBox) {
contentDescription = if (inAList) { contentDescription = getString(
setImageResource(app.pachli.core.ui.R.drawable.ic_clear_24dp) if (inAList) R.string.action_remove_from_list else R.string.action_add_to_list,
getString(R.string.action_remove_from_list) )
} else { isChecked = inAList
setImageResource(app.pachli.core.ui.R.drawable.ic_plus_24dp)
getString(R.string.action_add_to_list)
}
} }
} }
} }
companion object { companion object {
private const val ARG_LIST_ID = "listId" private const val ARG_LIST_ID = "app.pachli.ARG_LIST_ID"
private const val ARG_LIST_NAME = "listName" private const val ARG_LIST_NAME = "app.pachli.ARG_LIST_NAME"
@JvmStatic @JvmStatic
fun newInstance(listId: String, listName: String): AccountsInListFragment { fun newInstance(listId: String, listName: String): AccountsInListFragment {

View File

@ -31,7 +31,6 @@ import androidx.recyclerview.widget.ListAdapter
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible
import app.pachli.core.designsystem.R as DR import app.pachli.core.designsystem.R as DR
import app.pachli.core.ui.BackgroundMessage import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.BindingHolder import app.pachli.core.ui.BindingHolder
@ -49,7 +48,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
* Shows all the user's lists with a button to allow them to add/remove the given * Shows all the user's lists with a checkbox to allow them to add/remove the given
* account from each list. * account from each list.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
@ -189,23 +188,30 @@ class ListsForAccountFragment : DialogFragment() {
): BindingHolder<ItemAddOrRemoveFromListBinding> { ): BindingHolder<ItemAddOrRemoveFromListBinding> {
val binding = val binding =
ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false) ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding) val holder = BindingHolder(binding)
binding.checkBox.setOnCheckedChangeListener { _, isChecked ->
val item = getItem(holder.bindingAdapterPosition)
if (isChecked == item.isMember) return@setOnCheckedChangeListener
if (isChecked) {
viewModel.addAccountToList(item.list.id)
} else {
viewModel.deleteAccountFromList(item.list.id)
}
}
return holder
} }
override fun onBindViewHolder(holder: BindingHolder<ItemAddOrRemoveFromListBinding>, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAddOrRemoveFromListBinding>, position: Int) {
val item = getItem(position) val item = getItem(position)
holder.binding.listNameView.text = item.list.title holder.binding.listNameView.text = item.list.title
holder.binding.addButton.apply {
visible(!item.isMember) with(holder.binding.checkBox) {
setOnClickListener { contentDescription = getString(
viewModel.addAccountToList(item.list.id) if (item.isMember) R.string.action_remove_from_list else R.string.action_add_to_list,
} )
} isChecked = item.isMember
holder.binding.removeButton.apply {
visible(item.isMember)
setOnClickListener {
viewModel.deleteAccountFromList(item.list.id)
}
} }
} }
} }

View File

@ -23,6 +23,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dp"
android:paddingBottom="8dp"> android:paddingBottom="8dp">
<ImageView <ImageView
@ -34,6 +35,7 @@
android:contentDescription="@string/action_view_profile" android:contentDescription="@string/action_view_profile"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@drawable/avatar_default" /> tools:src="@drawable/avatar_default" />
<ImageView <ImageView
@ -46,23 +48,24 @@
app:layout_constraintEnd_toEndOf="@id/avatar" /> app:layout_constraintEnd_toEndOf="@id/avatar" />
<TextView <TextView
android:id="@+id/displayNameTextView" android:id="@+id/displayName"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:layout_marginTop="6dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
android:textStyle="normal|bold" android:textStyle="normal|bold"
app:layout_constraintEnd_toStartOf="@id/removeButton" app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintEnd_toStartOf="@+id/checkBox"
app:layout_constraintStart_toEndOf="@+id/avatar" app:layout_constraintStart_toEndOf="@+id/avatar"
app:layout_constraintTop_toTopOf="@id/avatar" app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Display name" /> tools:text="Display name" />
<TextView <TextView
android:id="@+id/usernameTextView" android:id="@+id/username"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
@ -70,36 +73,19 @@
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@+id/removeButton" app:layout_constraintEnd_toEndOf="@+id/displayName"
app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/displayNameTextView" app:layout_constraintTop_toBottomOf="@id/displayName"
tools:text="\@username" /> tools:text="\@username" />
<ImageButton <CheckBox
android:id="@+id/removeButton" android:id="@+id/checkBox"
style="@style/AppImageButton" android:layout_width="wrap_content"
android:layout_width="52dp" android:layout_height="0dp"
android:layout_height="48dp" android:minWidth="48dp"
android:layout_marginStart="12dp" android:minHeight="48dp"
android:background="?attr/selectableItemBackgroundBorderless" app:layout_constraintBottom_toBottomOf="@+id/avatar"
android:contentDescription="@string/action_remove_from_list"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/addButton"
app:layout_constraintStart_toEndOf="@id/displayNameTextView"
app:layout_constraintTop_toTopOf="@id/avatar"
app:srcCompat="@drawable/ic_clear_24dp" />
<ImageButton
android:id="@+id/addButton"
style="@style/AppImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_add_to_list"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/removeButton" app:layout_constraintTop_toTopOf="@+id/avatar" />
app:srcCompat="@drawable/ic_check_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,8 +6,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="16dp" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingVertical="8dp"> android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView <TextView
android:id="@+id/listNameView" android:id="@+id/listNameView"
@ -18,33 +20,16 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_list" app:drawableStartCompat="@drawable/ic_list"
app:drawableTint="?android:attr/textColorSecondary" app:drawableTint="?android:attr/textColorSecondary"
tools:text="Example list" /> tools:text="Example list" />
<ImageButton <CheckBox
android:id="@+id/addButton" android:id="@+id/checkBox"
style="@style/AppImageButton" android:layout_width="wrap_content"
android:layout_width="32dp" android:layout_height="wrap_content"
android:layout_height="32dp" android:minWidth="48dp"
android:layout_marginStart="12dp" android:minHeight="48dp" />
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_add_to_list"
android:padding="4dp"
android:src="@drawable/ic_plus_24dp" />
<ImageButton
android:id="@+id/removeButton"
style="@style/AppImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_remove_from_list"
android:padding="4dp"
android:src="@drawable/ic_clear_24dp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>