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 <eva@tatarka.me>
Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
This commit is contained in:
Eliot Lash 2024-06-09 11:25:03 -07:00 committed by GitHub
parent 8584e72f48
commit 9883bfa7c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 118 additions and 10 deletions

View File

@ -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

View File

@ -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

View File

@ -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");
}
};
}

View File

@ -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,

View File

@ -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()
}

View File

@ -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"

View File

@ -335,10 +335,11 @@
<string name="pref_summary_http_proxy_missing">&lt;not set></string>
<string name="pref_summary_http_proxy_invalid">&lt;invalid></string>
<string name="pref_default_post_privacy">Default post privacy</string>
<string name="pref_default_post_language">Default posting language</string>
<string name="pref_default_media_sensitivity">Always mark media as sensitive</string>
<string name="pref_publishing">Publishing (synced with server)</string>
<string name="pref_default_post_privacy">Default post privacy (synced with server)</string>
<string name="pref_default_post_language">Default posting language (synced with server)</string>
<string name="pref_default_reply_privacy">Default reply privacy (not synced with server)</string>
<string name="pref_default_media_sensitivity">Always mark media as sensitive (synced with server)</string>
<string name="pref_publishing">Publishing</string>
<string name="pref_failed_to_sync">Failed to sync preferences</string>
<string name="pref_main_nav_position">Main navigation position</string>

View File

@ -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)
}
}