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:
parent
8584e72f48
commit
9883bfa7c2
|
@ -182,7 +182,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaList + mediaItem
|
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 {
|
viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
|
@ -193,6 +194,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
is UploadEvent.ProgressEvent ->
|
is UploadEvent.ProgressEvent ->
|
||||||
item.copy(uploadPercent = event.percentage)
|
item.copy(uploadPercent = event.percentage)
|
||||||
|
|
||||||
is UploadEvent.FinishedEvent ->
|
is UploadEvent.FinishedEvent ->
|
||||||
item.copy(
|
item.copy(
|
||||||
id = event.mediaId,
|
id = event.mediaId,
|
||||||
|
@ -455,6 +457,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
emptyList()
|
emptyList()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
':' -> {
|
':' -> {
|
||||||
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
||||||
val incomplete = token.substring(1)
|
val incomplete = token.substring(1)
|
||||||
|
@ -467,6 +470,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
AutocompleteResult.EmojiResult(emoji)
|
AutocompleteResult.EmojiResult(emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unexpected autocompletion token: $token")
|
Log.w(TAG, "Unexpected autocompletion token: $token")
|
||||||
emptyList()
|
emptyList()
|
||||||
|
@ -480,16 +484,17 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
composeKind = composeOptions?.kind ?: ComposeKind.NEW
|
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
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||||
startingVisibility = Status.Visibility.byNum(
|
startingVisibility = Status.Visibility.byNum(
|
||||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
||||||
)
|
)
|
||||||
|
|
||||||
inReplyToId = composeOptions?.inReplyToId
|
|
||||||
|
|
||||||
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
||||||
|
|
||||||
val contentWarning = composeOptions?.contentWarning
|
val contentWarning = composeOptions?.contentWarning
|
||||||
|
|
|
@ -31,6 +31,7 @@ import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
|
||||||
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity
|
import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity
|
||||||
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
import com.keylesspalace.tusky.components.filters.FiltersActivity
|
||||||
|
@ -179,6 +180,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setEntries(R.array.post_privacy_names)
|
setEntries(R.array.post_privacy_names)
|
||||||
setEntryValues(R.array.post_privacy_values)
|
setEntryValues(R.array.post_privacy_values)
|
||||||
key = PrefKeys.DEFAULT_POST_PRIVACY
|
key = PrefKeys.DEFAULT_POST_PRIVACY
|
||||||
|
isSingleLineTitle = false
|
||||||
setSummaryProvider { entry }
|
setSummaryProvider { entry }
|
||||||
val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC
|
val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC
|
||||||
value = visibility.serverString
|
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 {
|
listPreference {
|
||||||
val locales =
|
val locales =
|
||||||
getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
|
getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
|
||||||
|
@ -204,6 +231,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
).toTypedArray()
|
).toTypedArray()
|
||||||
entryValues = (listOf("") + locales.map { it.language }).toTypedArray()
|
entryValues = (listOf("") + locales.map { it.language }).toTypedArray()
|
||||||
key = PrefKeys.DEFAULT_POST_LANGUAGE
|
key = PrefKeys.DEFAULT_POST_LANGUAGE
|
||||||
|
isSingleLineTitle = false
|
||||||
icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize)
|
icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize)
|
||||||
value = accountManager.activeAccount?.defaultPostLanguage.orEmpty()
|
value = accountManager.activeAccount?.defaultPostLanguage.orEmpty()
|
||||||
isPersistent = false // This will be entirely server-driven
|
isPersistent = false // This will be entirely server-driven
|
||||||
|
|
|
@ -62,7 +62,7 @@ import java.io.File;
|
||||||
},
|
},
|
||||||
// Note: Starting with version 54, database versions in Tusky are always even.
|
// Note: Starting with version 54, database versions in Tusky are always even.
|
||||||
// This is to reserve odd version numbers for use by forks.
|
// This is to reserve odd version numbers for use by forks.
|
||||||
version = 60,
|
version = 62,
|
||||||
autoMigrations = {
|
autoMigrations = {
|
||||||
@AutoMigration(from = 48, to = 49),
|
@AutoMigration(from = 48, to = 49),
|
||||||
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),
|
@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");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ data class AccountEntity(
|
||||||
var notificationVibration: Boolean = true,
|
var notificationVibration: Boolean = true,
|
||||||
var notificationLight: Boolean = true,
|
var notificationLight: Boolean = true,
|
||||||
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||||
|
var defaultReplyPrivacy: Status.Visibility = Status.Visibility.UNLISTED,
|
||||||
var defaultMediaSensitivity: Boolean = false,
|
var defaultMediaSensitivity: Boolean = false,
|
||||||
var defaultPostLanguage: String = "",
|
var defaultPostLanguage: String = "",
|
||||||
var alwaysShowSensitiveMedia: Boolean = false,
|
var alwaysShowSensitiveMedia: Boolean = false,
|
||||||
|
|
|
@ -65,7 +65,7 @@ object StorageModule {
|
||||||
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44,
|
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_44_45, AppDatabase.MIGRATION_45_46, AppDatabase.MIGRATION_46_47,
|
||||||
AppDatabase.MIGRATION_47_48, AppDatabase.MIGRATION_52_53, AppDatabase.MIGRATION_54_56,
|
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()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ object PrefKeys {
|
||||||
|
|
||||||
const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy"
|
const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy"
|
||||||
const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage"
|
const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage"
|
||||||
|
const val DEFAULT_REPLY_PRIVACY = "defaultReplyPrivacy"
|
||||||
const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity"
|
const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity"
|
||||||
const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled"
|
const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled"
|
||||||
const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia"
|
const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia"
|
||||||
|
|
|
@ -335,10 +335,11 @@
|
||||||
<string name="pref_summary_http_proxy_missing"><not set></string>
|
<string name="pref_summary_http_proxy_missing"><not set></string>
|
||||||
<string name="pref_summary_http_proxy_invalid"><invalid></string>
|
<string name="pref_summary_http_proxy_invalid"><invalid></string>
|
||||||
|
|
||||||
<string name="pref_default_post_privacy">Default post privacy</string>
|
<string name="pref_default_post_privacy">Default post privacy (synced with server)</string>
|
||||||
<string name="pref_default_post_language">Default posting language</string>
|
<string name="pref_default_post_language">Default posting language (synced with server)</string>
|
||||||
<string name="pref_default_media_sensitivity">Always mark media as sensitive</string>
|
<string name="pref_default_reply_privacy">Default reply privacy (not synced with server)</string>
|
||||||
<string name="pref_publishing">Publishing (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_failed_to_sync">Failed to sync preferences</string>
|
||||||
|
|
||||||
<string name="pref_main_nav_position">Main navigation position</string>
|
<string name="pref_main_nav_position">Main navigation position</string>
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue