From 9883bfa7c2679df1a5cc523b158c27415f39be02 Mon Sep 17 00:00:00 2001 From: Eliot Lash Date: Sun, 9 Jun 2024 11:25:03 -0700 Subject: [PATCH] Add option for default reply privacy set to unlisted by default (#4496) This PR fixes https://github.com/tuskyapp/Tusky/issues/2798 and is mostly based on and supersedes https://github.com/tuskyapp/Tusky/pull/2826 but I have fixed all merge conflicts and unit tests. I tested the changes locally and the setting takes effect immediately for replies, and persists across killing the app. --------- Co-authored-by: Eva Tatarka Co-authored-by: Konrad Pozniak --- .../components/compose/ComposeViewModel.kt | 13 ++-- .../preference/AccountPreferencesFragment.kt | 28 ++++++++ .../keylesspalace/tusky/db/AppDatabase.java | 9 ++- .../tusky/db/entity/AccountEntity.kt | 1 + .../keylesspalace/tusky/di/StorageModule.kt | 2 +- .../tusky/settings/SettingsConstants.kt | 1 + app/src/main/res/values/strings.xml | 9 +-- .../compose/ComposeViewModelTest.kt | 65 +++++++++++++++++++ 8 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.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 f9fabfd72..755583c6a 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 @@ -182,7 +182,8 @@ class ComposeViewModel @Inject constructor( mediaList + mediaItem } } - val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that + val mediaItem = + stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that viewModelScope.launch { mediaUploader @@ -193,6 +194,7 @@ class ComposeViewModel @Inject constructor( val newMediaItem = when (event) { is UploadEvent.ProgressEvent -> item.copy(uploadPercent = event.percentage) + is UploadEvent.FinishedEvent -> item.copy( id = event.mediaId, @@ -455,6 +457,7 @@ class ComposeViewModel @Inject constructor( emptyList() }) } + ':' -> { val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList() val incomplete = token.substring(1) @@ -467,6 +470,7 @@ class ComposeViewModel @Inject constructor( AutocompleteResult.EmojiResult(emoji) } } + else -> { Log.w(TAG, "Unexpected autocompletion token: $token") emptyList() @@ -480,16 +484,17 @@ class ComposeViewModel @Inject constructor( } composeKind = composeOptions?.kind ?: ComposeKind.NEW + inReplyToId = composeOptions?.inReplyToId - val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy + val activeAccount = accountManager.activeAccount!! + val preferredVisibility = + if (inReplyToId != null) activeAccount.defaultReplyPrivacy else activeAccount.defaultPostPrivacy val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN startingVisibility = Status.Visibility.byNum( preferredVisibility.num.coerceAtLeast(replyVisibility.num) ) - inReplyToId = composeOptions?.inReplyToId - modifiedInitialState = composeOptions?.modifiedInitialState == true val contentWarning = composeOptions?.contentWarning 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 708e44a1a..c50d0376e 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 @@ -31,6 +31,7 @@ import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.accountlist.AccountListActivity import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity import com.keylesspalace.tusky.components.filters.FiltersActivity @@ -179,6 +180,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setEntries(R.array.post_privacy_names) setEntryValues(R.array.post_privacy_values) key = PrefKeys.DEFAULT_POST_PRIVACY + isSingleLineTitle = false setSummaryProvider { entry } val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC value = visibility.serverString @@ -192,6 +194,31 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { } } + val activeAccount = accountManager.activeAccount + if (activeAccount != null) { + listPreference { + setTitle(R.string.pref_default_reply_privacy) + setEntries(R.array.post_privacy_names) + setEntryValues(R.array.post_privacy_values) + key = PrefKeys.DEFAULT_REPLY_PRIVACY + isSingleLineTitle = false + setSummaryProvider { entry } + val visibility = activeAccount.defaultReplyPrivacy + value = visibility.serverString + setIcon(getIconForVisibility(visibility)) + setOnPreferenceChangeListener { _, newValue -> + val newVisibility = Status.Visibility.byString(newValue as String) + setIcon(getIconForVisibility(newVisibility)) + activeAccount.defaultReplyPrivacy = newVisibility + accountManager.saveAccount(activeAccount) + viewLifecycleOwner.lifecycleScope.launch { + eventHub.dispatch(PreferenceChangedEvent(key)) + } + true + } + } + } + listPreference { val locales = getLocaleList(getInitialLanguages(null, accountManager.activeAccount)) @@ -204,6 +231,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { ).toTypedArray() entryValues = (listOf("") + locales.map { it.language }).toTypedArray() key = PrefKeys.DEFAULT_POST_LANGUAGE + isSingleLineTitle = false icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize) value = accountManager.activeAccount?.defaultPostLanguage.orEmpty() isPersistent = false // This will be entirely server-driven 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 7bf0cafce..879e168d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -62,7 +62,7 @@ import java.io.File; }, // Note: Starting with version 54, database versions in Tusky are always even. // This is to reserve odd version numbers for use by forks. - version = 60, + version = 62, autoMigrations = { @AutoMigration(from = 48, to = 49), @AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class), @@ -841,4 +841,11 @@ 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"); + } + }; } 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 d9d8f15f1..390c15f34 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 @@ -63,6 +63,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 defaultMediaSensitivity: Boolean = false, var defaultPostLanguage: String = "", var alwaysShowSensitiveMedia: Boolean = false, diff --git a/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt index 4086aefd5..5f5893b3b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt @@ -65,7 +65,7 @@ object StorageModule { AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44, AppDatabase.MIGRATION_44_45, AppDatabase.MIGRATION_45_46, AppDatabase.MIGRATION_46_47, AppDatabase.MIGRATION_47_48, AppDatabase.MIGRATION_52_53, AppDatabase.MIGRATION_54_56, - AppDatabase.MIGRATION_58_60 + AppDatabase.MIGRATION_58_60, AppDatabase.MIGRATION_60_62 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index e6061e6b2..4861f5978 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -86,6 +86,7 @@ object PrefKeys { const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy" const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage" + const val DEFAULT_REPLY_PRIVACY = "defaultReplyPrivacy" const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity" const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled" const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ffef2cf3..531b9185d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -335,10 +335,11 @@ <not set> <invalid> - Default post privacy - Default posting language - Always mark media as sensitive - Publishing (synced with server) + Default post privacy (synced with server) + Default posting language (synced with server) + Default reply privacy (not synced with server) + Always mark media as sensitive (synced with server) + Publishing Failed to sync preferences Main navigation position 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 new file mode 100644 index 000000000..96f85c5ed --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt @@ -0,0 +1,65 @@ +package com.keylesspalace.tusky.components.compose + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.appstore.EventHub +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 org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class ComposeViewModelTest { + + private lateinit var api: MastodonApi + private lateinit var accountManager: AccountManager + private lateinit var eventHub: EventHub + private lateinit var viewModel: ComposeViewModel + + @Before + fun setup() { + api = mock() + accountManager = mock { + on { activeAccount } doReturn + AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + eventHub = EventHub() + + viewModel = ComposeViewModel( + api = api, + accountManager = accountManager, + mediaUploader = mock(), + serviceClient = mock(), + draftHelper = mock(), + instanceInfoRepo = mock(), + ) + } + + @Test + fun `startingVisibility initially set to defaultPostPrivacy for post`() { + viewModel.setup(null) + + assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value) + } + + @Test + fun `startingVisibility initially set to replyPostPrivacy for reply`() { + viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123")) + + assertEquals(Status.Visibility.UNLISTED, viewModel.statusVisibility.value) + } +}