From bdbe2f85c2925dd65eed9836f1ad9a11ac27c326 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 14 Mar 2024 23:56:16 +0100 Subject: [PATCH] feat: Allow the user to set a list's replies policy (#534) The replies policy controls whether replies from members of the list also appear in the list. Display the replies policy as three radio buttons when a list is created or updated, and send the chosen replies policy via the API. Default value if not specified is always "list", for consistency with the Mastodon API defaults. While I'm here: - Ensure the list dialog layout is inflated using the dialog's themed context - Use a `TextInputLayout` wrapper around the list name in the list dialog for better UX - Simplify the dialog layout, use LinearLayout, and standard padding and margins --- app/src/main/java/app/pachli/ListsActivity.kt | 38 ++++++++-- .../app/pachli/viewmodel/ListsViewModel.kt | 9 ++- app/src/main/res/layout/dialog_list.xml | 73 ++++++++++++------- app/src/main/res/values/donottranslate.xml | 1 - app/src/main/res/values/strings.xml | 5 ++ .../core/data/repository/ListsRepository.kt | 5 +- .../data/repository/NetworkListsRepository.kt | 9 ++- .../pachli/core/network/model/MastoList.kt | 29 ++++++++ .../core/network/retrofit/MastodonApi.kt | 3 + 9 files changed, 129 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/app/pachli/ListsActivity.kt b/app/src/main/java/app/pachli/ListsActivity.kt index d475cb50e..a134e5733 100644 --- a/app/src/main/java/app/pachli/ListsActivity.kt +++ b/app/src/main/java/app/pachli/ListsActivity.kt @@ -46,6 +46,7 @@ import app.pachli.core.data.repository.Lists import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.network.model.MastoList +import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.retrofit.apiresult.ApiError import app.pachli.core.network.retrofit.apiresult.NetworkError import app.pachli.core.ui.await @@ -149,10 +150,9 @@ class ListsActivity : BaseActivity(), MenuProvider { } private suspend fun showListNameDialog(list: MastoList?) { - val binding = DialogListBinding.inflate(layoutInflater) - val dialog = AlertDialog.Builder(this) - .setView(binding.root) - .create() + val builder = AlertDialog.Builder(this) + val binding = DialogListBinding.inflate(LayoutInflater.from(builder.context)) + val dialog = builder.setView(binding.root).create() // Ensure the soft keyboard opens when the name field has focus dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) @@ -172,6 +172,8 @@ class ListsActivity : BaseActivity(), MenuProvider { } ?: binding.exclusiveCheckbox.hide() } + binding.repliesPolicyGroup.check(list?.repliesPolicy?.resourceId() ?: UserListRepliesPolicy.LIST.resourceId()) + binding.nameText.requestFocus() } @@ -185,6 +187,7 @@ class ListsActivity : BaseActivity(), MenuProvider { binding.nameText.text.toString(), list?.id, binding.exclusiveCheckbox.isChecked, + UserListRepliesPolicy.Companion.from(binding.repliesPolicyGroup.checkedRadioButtonId), ) } } @@ -327,11 +330,32 @@ class ListsActivity : BaseActivity(), MenuProvider { } } - private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean) { + private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy) { if (listId == null) { - viewModel.createNewList(name, exclusive) + viewModel.createNewList(name, exclusive, repliesPolicy) } else { - viewModel.updateList(listId, name, exclusive) + viewModel.updateList(listId, name, exclusive, repliesPolicy) + } + } + + /** @return The resource ID of the radio button for this replies policy */ + private fun UserListRepliesPolicy.resourceId() = when (this) { + UserListRepliesPolicy.FOLLOWED -> R.id.repliesPolicyFollowed + UserListRepliesPolicy.LIST -> R.id.repliesPolicyList + UserListRepliesPolicy.NONE -> R.id.repliesPolicyNone + } + + /** + * @return A [UserListRepliesPolicy] corresponding to [resourceId], which must + * be one of the resource IDs for a replies policy radio button + * @throws IllegalStateException if an unrecognised [resourceId] is used + */ + private fun UserListRepliesPolicy.Companion.from(resourceId: Int): UserListRepliesPolicy { + return when (resourceId) { + R.id.repliesPolicyFollowed -> UserListRepliesPolicy.FOLLOWED + R.id.repliesPolicyList -> UserListRepliesPolicy.LIST + R.id.repliesPolicyNone -> UserListRepliesPolicy.NONE + else -> throw IllegalStateException("unknown resource id") } } } diff --git a/app/src/main/java/app/pachli/viewmodel/ListsViewModel.kt b/app/src/main/java/app/pachli/viewmodel/ListsViewModel.kt index 843f4a965..4906782df 100644 --- a/app/src/main/java/app/pachli/viewmodel/ListsViewModel.kt +++ b/app/src/main/java/app/pachli/viewmodel/ListsViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.pachli.core.data.repository.ListsError import app.pachli.core.data.repository.ListsRepository +import app.pachli.core.network.model.UserListRepliesPolicy import com.github.michaelbull.result.onFailure import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -61,18 +62,18 @@ internal class ListsViewModel @Inject constructor( listsRepository.refresh() } - fun createNewList(title: String, exclusive: Boolean) = viewModelScope.launch { + fun createNewList(title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy) = viewModelScope.launch { _operationCount.getAndUpdate { it + 1 } - listsRepository.createList(title, exclusive).onFailure { + listsRepository.createList(title, exclusive, repliesPolicy).onFailure { _errors.send(Error.Create(title, it)) } }.invokeOnCompletion { _operationCount.getAndUpdate { it - 1 } } - fun updateList(listId: String, title: String, exclusive: Boolean) = viewModelScope.launch { + fun updateList(listId: String, title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy) = viewModelScope.launch { _operationCount.getAndUpdate { it + 1 } - listsRepository.editList(listId, title, exclusive).onFailure { + listsRepository.editList(listId, title, exclusive, repliesPolicy).onFailure { _errors.send(Error.Update(title, it)) } }.invokeOnCompletion { _operationCount.getAndUpdate { it - 1 } } diff --git a/app/src/main/res/layout/dialog_list.xml b/app/src/main/res/layout/dialog_list.xml index 5a75c1bed..fbb8267b8 100644 --- a/app/src/main/res/layout/dialog_list.xml +++ b/app/src/main/res/layout/dialog_list.xml @@ -1,37 +1,60 @@ - + android:orientation="vertical" + android:padding="?dialogPreferredPadding"> - + android:hint="@string/hint_list_name"> + + + + android:layout_marginTop="16dp" + android:text="@string/list_exclusive_label" /> - + + + + + + + + + + + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index ec4385f65..d4ae2ce79 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -293,5 +293,4 @@ ONCE_PER_VERSION ALWAYS - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9bce983c..e94b9a089 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -717,4 +717,9 @@ There are no updates available Next scheduled check: %1$s Could not download %1$s: %2$d %3$s + + Show replies to: + Any followed user + Members of the list + No one diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt index 88a643e0c..b10138557 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/ListsRepository.kt @@ -19,6 +19,7 @@ package app.pachli.core.data.repository import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.TimelineAccount +import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.retrofit.apiresult.ApiError import com.github.michaelbull.result.Result import java.text.Collator @@ -70,7 +71,7 @@ interface ListsRepository { * @param exclusive True if the list is exclusive * @return Details of the new list if successfuly, or an error */ - suspend fun createList(title: String, exclusive: Boolean): Result + suspend fun createList(title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy): Result /** * Edit an existing list. @@ -80,7 +81,7 @@ interface ListsRepository { * @param exclusive New exclusive vale for the list * @return Amended list, or an error */ - suspend fun editList(listId: String, title: String, exclusive: Boolean): Result + suspend fun editList(listId: String, title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy): Result /** * Delete an existing list diff --git a/core/data/src/main/kotlin/app/pachli/core/data/repository/NetworkListsRepository.kt b/core/data/src/main/kotlin/app/pachli/core/data/repository/NetworkListsRepository.kt index 5b6873b25..cc3fe47a8 100644 --- a/core/data/src/main/kotlin/app/pachli/core/data/repository/NetworkListsRepository.kt +++ b/core/data/src/main/kotlin/app/pachli/core/data/repository/NetworkListsRepository.kt @@ -27,6 +27,7 @@ import app.pachli.core.data.repository.ListsError.Update import app.pachli.core.database.model.TabData import app.pachli.core.network.model.MastoList import app.pachli.core.network.model.TimelineAccount +import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.retrofit.MastodonApi import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok @@ -122,18 +123,18 @@ class NetworkListsRepository @Inject constructor( } } - override suspend fun createList(title: String, exclusive: Boolean): Result = binding { + override suspend fun createList(title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy): Result = binding { externalScope.async { - api.createList(title, exclusive).mapError { Create(it) }.bind().run { + api.createList(title, exclusive, repliesPolicy).mapError { Create(it) }.bind().run { refresh() body } }.await() } - override suspend fun editList(listId: String, title: String, exclusive: Boolean): Result = binding { + override suspend fun editList(listId: String, title: String, exclusive: Boolean, repliesPolicy: UserListRepliesPolicy): Result = binding { externalScope.async { - api.updateList(listId, title, exclusive).mapError { Update(it) }.bind().run { + api.updateList(listId, title, exclusive, repliesPolicy).mapError { Update(it) }.bind().run { refresh() body } diff --git a/core/network/src/main/kotlin/app/pachli/core/network/model/MastoList.kt b/core/network/src/main/kotlin/app/pachli/core/network/model/MastoList.kt index bea927c80..e9dddfacf 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/model/MastoList.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/model/MastoList.kt @@ -16,8 +16,34 @@ package app.pachli.core.network.model +import app.pachli.core.network.json.Default +import app.pachli.core.network.json.HasDefault +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +/** https://docs.joinmastodon.org/entities/List/#replies_policy */ +@HasDefault +enum class UserListRepliesPolicy { + /** Show replies to any followed user */ + @Json(name = "followed") + FOLLOWED, + + /** Show replies to members of the list */ + @Default + @Json(name = "list") + LIST, + + /** Show replies to no one */ + @Json(name = "none") + NONE, + + ; + + // Empty companion object so that other code can add extension functions + // to this enum. See e.g., ListsActivity. + companion object +} + @JsonClass(generateAdapter = true) data class MastoList( val id: String, @@ -27,4 +53,7 @@ data class MastoList( * Null implies the server does not support this feature. */ val exclusive: Boolean? = null, + + @Json(name = "replies_policy") + val repliesPolicy: UserListRepliesPolicy = UserListRepliesPolicy.LIST, ) diff --git a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt index 7bfae0edd..d8852b93a 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt @@ -49,6 +49,7 @@ import app.pachli.core.network.model.TimelineAccount import app.pachli.core.network.model.Translation import app.pachli.core.network.model.TrendingTag import app.pachli.core.network.model.TrendsLink +import app.pachli.core.network.model.UserListRepliesPolicy import app.pachli.core.network.retrofit.apiresult.ApiResult import at.connyduck.calladapter.networkresult.NetworkResult import okhttp3.MultipartBody @@ -557,6 +558,7 @@ interface MastodonApi { suspend fun createList( @Field("title") title: String, @Field("exclusive") exclusive: Boolean?, + @Field("replies_policy") repliesPolicy: UserListRepliesPolicy = UserListRepliesPolicy.LIST, ): ApiResult @FormUrlEncoded @@ -565,6 +567,7 @@ interface MastodonApi { @Path("listId") listId: String, @Field("title") title: String, @Field("exclusive") exclusive: Boolean?, + @Field("replies_policy") repliesPolicy: UserListRepliesPolicy = UserListRepliesPolicy.LIST, ): ApiResult @DELETE("api/v1/lists/{listId}")