From 892801b83af1bd6be0127684e6acaf7fc209c586 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 2 Aug 2024 17:15:10 +0200 Subject: [PATCH] add more options to default reply visibility setting (#4568) This adds `direct` and `match_default_post_visibility` as options to the default reply visibility setting. `match_default_post_visibility` will be the default for new accounts. closes https://github.com/tuskyapp/Tusky/issues/4555 --- .../components/compose/ComposeViewModel.kt | 15 +++--- .../preference/AccountPreferencesFragment.kt | 28 ++++++---- .../keylesspalace/tusky/db/AppDatabase.java | 2 +- .../com/keylesspalace/tusky/db/Converters.kt | 15 +++++- .../tusky/db/entity/AccountEntity.kt | 3 +- .../com/keylesspalace/tusky/entity/Status.kt | 13 ++--- .../receiver/SendStatusBroadcastReceiver.kt | 2 +- .../tusky/service/SendStatusService.kt | 2 +- .../tusky/settings/DefaultReplyVisibility.kt | 51 +++++++++++++++++++ app/src/main/res/values/donottranslate.xml | 8 +++ app/src/main/res/values/string-arrays.xml | 8 +++ app/src/main/res/values/strings.xml | 5 +- .../compose/ComposeViewModelTest.kt | 18 +++++-- 13 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/settings/DefaultReplyVisibility.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 755583c6a..6b15b1e50 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -390,7 +390,7 @@ class ComposeViewModel @Inject constructor( val tootToSend = StatusToSend( text = content, warningText = spoilerText, - visibility = _statusVisibility.value.serverString, + visibility = _statusVisibility.value.stringValue, sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value), media = attachedMedia, scheduledAt = _scheduledAt.value, @@ -487,12 +487,15 @@ class ComposeViewModel @Inject constructor( inReplyToId = composeOptions?.inReplyToId val activeAccount = accountManager.activeAccount!! - val preferredVisibility = - if (inReplyToId != null) activeAccount.defaultReplyPrivacy else activeAccount.defaultPostPrivacy + val preferredVisibility = if (inReplyToId != null) { + activeAccount.defaultReplyPrivacy.toVisibilityOr(activeAccount.defaultPostPrivacy) + } else { + activeAccount.defaultPostPrivacy + } val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN - startingVisibility = Status.Visibility.byNum( - preferredVisibility.num.coerceAtLeast(replyVisibility.num) + startingVisibility = Status.Visibility.fromInt( + preferredVisibility.int.coerceAtLeast(replyVisibility.int) ) modifiedInitialState = composeOptions?.modifiedInitialState == true @@ -534,7 +537,7 @@ class ComposeViewModel @Inject constructor( postLanguage = composeOptions?.language val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN - if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { + if (tootVisibility.int != Status.Visibility.UNKNOWN.int) { startingVisibility = tootVisibility } _statusVisibility.value = startingVisibility diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt index efa047463..9d1cdc107 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.util.Log import androidx.annotation.DrawableRes import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference import androidx.preference.PreferenceFragmentCompat import at.connyduck.calladapter.networkresult.fold import com.google.android.material.color.MaterialColors @@ -43,6 +44,7 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.settings.AccountPreferenceDataStore +import com.keylesspalace.tusky.settings.DefaultReplyVisibility import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.listPreference import com.keylesspalace.tusky.settings.makePreferenceScreen @@ -183,13 +185,15 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { isSingleLineTitle = false setSummaryProvider { entry } val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC - value = visibility.serverString + value = visibility.stringValue setIcon(getIconForVisibility(visibility)) isPersistent = false // its saved to the account and shouldn't be in shared preferences setOnPreferenceChangeListener { _, newValue -> - setIcon( - getIconForVisibility(Status.Visibility.byString(newValue as String)) - ) + val icon = getIconForVisibility(Status.Visibility.fromStringValue(newValue as String)) + setIcon(icon) + if (accountManager.activeAccount?.defaultReplyPrivacy == DefaultReplyVisibility.MATCH_DEFAULT_POST_VISIBILITY) { + findPreference(PrefKeys.DEFAULT_REPLY_PRIVACY)?.setIcon(icon) + } syncWithServer(visibility = newValue) true } @@ -199,18 +203,18 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { if (activeAccount != null) { listPreference { setTitle(R.string.pref_default_reply_privacy) - setEntries(R.array.post_privacy_names) - setEntryValues(R.array.post_privacy_values) + setEntries(R.array.reply_privacy_names) + setEntryValues(R.array.reply_privacy_values) key = PrefKeys.DEFAULT_REPLY_PRIVACY isSingleLineTitle = false setSummaryProvider { entry } val visibility = activeAccount.defaultReplyPrivacy - value = visibility.serverString - setIcon(getIconForVisibility(visibility)) + value = visibility.stringValue + setIcon(getIconForVisibility(visibility.toVisibilityOr(activeAccount.defaultPostPrivacy))) isPersistent = false // its saved to the account and shouldn't be in shared preferences setOnPreferenceChangeListener { _, newValue -> - val newVisibility = Status.Visibility.byString(newValue as String) - setIcon(getIconForVisibility(newVisibility)) + val newVisibility = DefaultReplyVisibility.fromStringValue(newValue as String) + setIcon(getIconForVisibility(newVisibility.toVisibilityOr(activeAccount.defaultPostPrivacy))) activeAccount.defaultReplyPrivacy = newVisibility accountManager.saveAccount(activeAccount) viewLifecycleOwner.lifecycleScope.launch { @@ -219,6 +223,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { true } } + preference { + setSummary(R.string.pref_default_reply_privacy_explanation) + isEnabled = false + } } listPreference { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 2ade04712..81597e00c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -846,7 +846,7 @@ public abstract class AppDatabase extends RoomDatabase { public static final Migration MIGRATION_60_62 = new Migration(60, 62) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultReplyPrivacy` INTEGER NOT NULL DEFAULT 2"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultReplyPrivacy` INTEGER NOT NULL DEFAULT 0"); } }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 6280cc5cf..5f36c0a77 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.DefaultReplyVisibility import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import java.net.URLDecoder @@ -56,12 +57,22 @@ class Converters @Inject constructor( @TypeConverter fun visibilityToInt(visibility: Status.Visibility?): Int { - return visibility?.num ?: Status.Visibility.UNKNOWN.num + return visibility?.int ?: Status.Visibility.UNKNOWN.int } @TypeConverter fun intToVisibility(visibility: Int): Status.Visibility { - return Status.Visibility.byNum(visibility) + return Status.Visibility.fromInt(visibility) + } + + @TypeConverter + fun defaultReplyVisibilityToInt(visibility: DefaultReplyVisibility?): Int { + return visibility?.int ?: DefaultReplyVisibility.MATCH_DEFAULT_POST_VISIBILITY.int + } + + @TypeConverter + fun intToDefaultReplyVisibility(visibility: Int): DefaultReplyVisibility { + return DefaultReplyVisibility.fromInt(visibility) } @TypeConverter diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt index 390c15f34..a77478812 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt @@ -25,6 +25,7 @@ import com.keylesspalace.tusky.db.Converters import com.keylesspalace.tusky.defaultTabs import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.DefaultReplyVisibility @Entity( indices = [ @@ -63,7 +64,7 @@ data class AccountEntity( var notificationVibration: Boolean = true, var notificationLight: Boolean = true, var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC, - var defaultReplyPrivacy: Status.Visibility = Status.Visibility.UNLISTED, + var defaultReplyPrivacy: DefaultReplyVisibility = DefaultReplyVisibility.MATCH_DEFAULT_POST_VISIBILITY, var defaultMediaSensitivity: Boolean = false, var defaultPostLanguage: String = "", var alwaysShowSensitiveMedia: Boolean = false, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index ccf617042..63e165934 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -68,7 +68,7 @@ data class Status( get() = reblog ?: this @JsonClass(generateAdapter = false) - enum class Visibility(val num: Int) { + enum class Visibility(val int: Int) { UNKNOWN(0), @Json(name = "public") @@ -83,7 +83,7 @@ data class Status( @Json(name = "direct") DIRECT(4); - val serverString: String + val stringValue: String get() = when (this) { PUBLIC -> "public" UNLISTED -> "unlisted" @@ -93,10 +93,8 @@ data class Status( } companion object { - - @JvmStatic - fun byNum(num: Int): Visibility { - return when (num) { + fun fromInt(int: Int): Visibility { + return when (int) { 4 -> DIRECT 3 -> PRIVATE 2 -> UNLISTED @@ -106,8 +104,7 @@ data class Status( } } - @JvmStatic - fun byString(s: String): Visibility { + fun fromStringValue(s: String): Visibility { return when (s) { "public" -> PUBLIC "unlisted" -> UNLISTED diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 16de9593d..8b8b7d8bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -93,7 +93,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { StatusToSend( text = text, warningText = spoiler, - visibility = visibility.serverString, + visibility = visibility.stringValue, sensitive = false, media = emptyList(), scheduledAt = null, diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 9d77acd4d..f7a98c5e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -393,7 +393,7 @@ class SendStatusService : Service() { content = status.text, contentWarning = status.warningText, sensitive = status.sensitive, - visibility = Status.Visibility.byString(status.visibility), + visibility = Status.Visibility.fromStringValue(status.visibility), mediaUris = status.media.map { it.uri }, mediaDescriptions = status.media.map { it.description }, mediaFocus = status.media.map { it.focus }, diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/DefaultReplyVisibility.kt b/app/src/main/java/com/keylesspalace/tusky/settings/DefaultReplyVisibility.kt new file mode 100644 index 000000000..9456b6c0d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/DefaultReplyVisibility.kt @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.settings + +import com.keylesspalace.tusky.entity.Status + +enum class DefaultReplyVisibility(val int: Int) { + MATCH_DEFAULT_POST_VISIBILITY(0), + PUBLIC(1), + UNLISTED(2), + PRIVATE(3), + DIRECT(4); + + val stringValue: String + get() = when (this) { + MATCH_DEFAULT_POST_VISIBILITY -> "match_default_post_visibility" + PUBLIC -> "public" + UNLISTED -> "unlisted" + PRIVATE -> "private" + DIRECT -> "direct" + } + + fun toVisibilityOr(default: Status.Visibility): Status.Visibility { + return when (this) { + PUBLIC -> Status.Visibility.PUBLIC + UNLISTED -> Status.Visibility.UNLISTED + PRIVATE -> Status.Visibility.PRIVATE + DIRECT -> Status.Visibility.DIRECT + else -> default + } + } + + companion object { + fun fromInt(int: Int): DefaultReplyVisibility { + return when (int) { + 4 -> DIRECT + 3 -> PRIVATE + 2 -> UNLISTED + 1 -> PUBLIC + else -> MATCH_DEFAULT_POST_VISIBILITY + } + } + fun fromStringValue(s: String): DefaultReplyVisibility { + return when (s) { + "public" -> PUBLIC + "unlisted" -> UNLISTED + "private" -> PRIVATE + "direct" -> DIRECT + else -> MATCH_DEFAULT_POST_VISIBILITY + } + } + } +} diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 237180e51..f4272d17f 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -21,6 +21,14 @@ private + + match_default_post_visibility + public + unlisted + private + direct + + smallest small diff --git a/app/src/main/res/values/string-arrays.xml b/app/src/main/res/values/string-arrays.xml index 59e997686..6ceac0a81 100644 --- a/app/src/main/res/values/string-arrays.xml +++ b/app/src/main/res/values/string-arrays.xml @@ -16,6 +16,14 @@ @string/post_privacy_followers_only + + @string/pref_match_default_post_privacy + @string/post_privacy_public + @string/post_privacy_unlisted + @string/post_privacy_followers_only + @string/post_privacy_direct + + @string/post_text_size_smallest @string/post_text_size_small diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd9388f91..0fcdec95a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,8 +341,10 @@ Default post privacy (synced with server) Default posting language (synced with server) Default reply privacy (not synced with server) + The pre-selected privacy will depend on the post you are replying to. Always mark media as sensitive (synced with server) - Publishing + Match default post privacy + Posting defaults Failed to sync preferences Main navigation position @@ -353,6 +355,7 @@ Public Unlisted Followers-only + Direct UI text size Post text size diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt index 96f85c5ed..0afcbb76b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt @@ -6,8 +6,8 @@ import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.entity.AccountEntity import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.DefaultReplyVisibility import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.doReturn @@ -23,8 +23,7 @@ class ComposeViewModelTest { private lateinit var eventHub: EventHub private lateinit var viewModel: ComposeViewModel - @Before - fun setup() { + private fun setup(defaultReplyVisibility: DefaultReplyVisibility = DefaultReplyVisibility.UNLISTED) { api = mock() accountManager = mock { on { activeAccount } doReturn @@ -34,7 +33,8 @@ class ComposeViewModelTest { accessToken = "fakeToken", clientId = "fakeId", clientSecret = "fakeSecret", - isActive = true + isActive = true, + defaultReplyPrivacy = defaultReplyVisibility ) } eventHub = EventHub() @@ -51,6 +51,7 @@ class ComposeViewModelTest { @Test fun `startingVisibility initially set to defaultPostPrivacy for post`() { + setup() viewModel.setup(null) assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value) @@ -58,8 +59,17 @@ class ComposeViewModelTest { @Test fun `startingVisibility initially set to replyPostPrivacy for reply`() { + setup() viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123")) assertEquals(Status.Visibility.UNLISTED, viewModel.statusVisibility.value) } + + @Test + fun `startingVisibility initially set to defaultPostPrivacy when replyPostPrivacy is MATCH_DEFAULT_POST_VISIBILITY for reply`() { + setup(defaultReplyVisibility = DefaultReplyVisibility.MATCH_DEFAULT_POST_VISIBILITY) + viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123")) + + assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value) + } }