fix: Disable "Scheduled post" support on GoToSocial accounts (#1025)

GoToSocial servers don't support scheduled posts; they return the wrong
type, and this can cause a loop of posting.

The GoToSocial bug to implement scheduled posts is
https://github.com/superseriousbusiness/gotosocial/issues/1006.

Fix this by adding a new server capability for scheduled post support,
using it for most servers, and disabling it for GoToSocial.

If scheduled post support is not available for an account:

- The "Scheduled posts" menu option is not shown.
- The scheduling button (clock) when composing a post is hidden, so the
user cannot set scheduling parameters.

Fixes #963
This commit is contained in:
Nik Clayton 2024-10-18 15:37:10 +02:00 committed by GitHub
parent 8fac5c3d4d
commit c8aa4fd374
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 17 deletions

View File

@ -81,9 +81,11 @@ import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.data.repository.Lists
import app.pachli.core.data.repository.ListsRepository
import app.pachli.core.data.repository.ListsRepository.Companion.compareByListTitle
import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.designsystem.EmbeddedFontFamily
import app.pachli.core.designsystem.R as DR
import app.pachli.core.model.ServerOperation
import app.pachli.core.model.Timeline
import app.pachli.core.navigation.AboutActivityIntent
import app.pachli.core.navigation.AccountActivityIntent
@ -160,12 +162,15 @@ import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.addItemAtPosition
import com.mikepenz.materialdrawer.util.addItems
import com.mikepenz.materialdrawer.util.addItemsAtPosition
import com.mikepenz.materialdrawer.util.getPosition
import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.hilt.android.AndroidEntryPoint
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
@ -205,6 +210,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
@Inject
lateinit var androidNotificationsAreEnabled: AndroidNotificationsAreEnabledUseCase
@Inject
lateinit var serverRepository: ServerRepository
private val binding by viewBinding(ActivityMainBinding::inflate)
override val actionButton by unsafeLazy { binding.composeButton }
@ -402,6 +410,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}
lifecycleScope.launch {
serverRepository.flow.collect {
refreshMainDrawerItems(intent.pachliAccountId, addSearchButton = hideTopToolbar)
}
}
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@ -749,6 +763,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
},
DividerDrawerItem(),
primaryDrawerItem {
identifier = DRAWER_ITEM_DRAFTS
nameRes = R.string.action_access_drafts
iconRes = R.drawable.ic_notebook
onClick = {
@ -757,15 +772,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
)
}
},
primaryDrawerItem {
nameRes = R.string.action_access_scheduled_posts
iconRes = R.drawable.ic_access_time
onClick = {
startActivityWithDefaultTransition(
ScheduledStatusActivityIntent(context, pachliAccountId),
)
}
},
primaryDrawerItem {
identifier = DRAWER_ITEM_ANNOUNCEMENTS
nameRes = R.string.title_announcements
@ -840,6 +846,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}
// If the server supports scheduled posts then add a "Scheduled posts" item
// after the "Drafts" item.
if (serverRepository.flow.replayCache.lastOrNull()?.get()?.can(ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED, ">= 1.0.0".toConstraint()) == true) {
binding.mainDrawer.addItemAtPosition(
binding.mainDrawer.getPosition(DRAWER_ITEM_DRAFTS) + 1,
primaryDrawerItem {
nameRes = R.string.action_access_scheduled_posts
iconRes = R.drawable.ic_access_time
onClick = {
startActivityWithDefaultTransition(
ScheduledStatusActivityIntent(binding.mainDrawer.context, pachliAccountId),
)
}
},
)
}
if (BuildConfig.DEBUG) {
// Add a "Developer tools" entry. Code that makes it easier to
// set the app state at runtime belongs here, it will never
@ -1260,6 +1283,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
/** Drawer identifier for the "Lists" section header. */
private const val DRAWER_ITEM_LISTS: Long = 15
/** Drawer identifier for the "Drafts" item. */
private const val DRAWER_ITEM_DRAFTS = 16L
}
}

View File

@ -505,6 +505,15 @@ class ComposeActivity :
}
}
// Hide the "Schedule" button if the server can't schedule. Simply
// disabling it could be confusing to users wondering why they can't
// use it.
lifecycleScope.launch {
viewModel.serverCanSchedule.collect {
binding.composeScheduleButton.visible(it)
}
}
lifecycleScope.launch {
viewModel.media.combine(viewModel.poll) { media, poll ->
val active = poll == null &&

View File

@ -35,6 +35,8 @@ import app.pachli.core.common.string.mastodonLength
import app.pachli.core.common.string.randomAlphanumericString
import app.pachli.core.data.repository.AccountManager
import app.pachli.core.data.repository.InstanceInfoRepository
import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.model.ServerOperation
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.ComposeKind
import app.pachli.core.network.model.Attachment
@ -49,17 +51,22 @@ import at.connyduck.calladapter.networkresult.fold
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import com.github.michaelbull.result.get
import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -73,6 +80,7 @@ class ComposeViewModel @Inject constructor(
private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper,
instanceInfoRepo: InstanceInfoRepository,
private val serverRepository: ServerRepository,
) : ViewModel() {
/** The current content */
@ -140,6 +148,11 @@ class ComposeViewModel @Inject constructor(
private val _statusLength = MutableStateFlow(0)
val statusLength = _statusLength.asStateFlow()
/** Flow of whether or not the server can schedule posts. */
val serverCanSchedule = serverRepository.flow.map {
it.get()?.can(ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED, ">= 1.0.0".toConstraint()) == true
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
private lateinit var composeKind: ComposeKind
// Used in ComposeActivity to pass state to result function when cropImage contract inflight

View File

@ -77,4 +77,13 @@ enum class ServerOperation(id: String, versions: List<Version>) {
ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY("org.joinmastodon.search.query:in:library", listOf(Version(major = 1))),
ORG_JOINMASTODON_SEARCH_QUERY_IN_PUBLIC("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))),
ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE("org.joinmastodon.search.query:in:public", listOf(Version(major = 1))),
/** Post a status with a `scheduled_at` property, and edit scheduled statuses. */
ORG_JOINMASTODON_STATUSES_SCHEDULED(
"org.joinmastodon.statuses.scheduled",
listOf(
// Initial introduction in Mastodon 2.7.0.
Version(major = 1),
),
),
}

View File

@ -52,6 +52,7 @@ import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_PU
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE
import app.pachli.core.network.Server.Error.UnparseableVersion
import app.pachli.core.network.model.InstanceV1
@ -241,6 +242,11 @@ data class Server(
when (kind) {
// Glitch has the same version number as upstream Mastodon
GLITCH, MASTODON -> {
// Scheduled statuses
when {
v >= "2.7.0".toVersion() -> c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
}
// Client filtering
when {
v >= "3.1.0".toVersion() -> c[ORG_JOINMASTODON_FILTERS_CLIENT] = "1.1.0".toVersion()
@ -281,6 +287,8 @@ data class Server(
}
GOTOSOCIAL -> {
// Can't do scheduled posts, https://github.com/superseriousbusiness/gotosocial/issues/1006
// Filters
when {
// Implemented in https://github.com/superseriousbusiness/gotosocial/pull/2936
@ -305,12 +313,18 @@ data class Server(
FIREFISH -> { }
// Sharkey can't filter, https://activitypub.software/TransFem-org/Sharkey/-/issues/492
SHARKEY -> { }
SHARKEY -> {
// Assume scheduled support (may be wrong).
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
}
FRIENDICA -> {
// Assume filter support (may be wrong)
// Assume filter support (may be wrong).
c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion()
// Assume scheduled support (may be wrong).
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
// Search
when {
// Friendica has a number of search operators that are not in Mastodon.
@ -323,10 +337,16 @@ data class Server(
}
}
// Everything else. Assume server side filtering and no translation. This may be an
// incorrect assumption.
// Everything else. Assume:
//
// - server side filtering
// - scheduled status support
// - no translation
//
// This may be an incorrect assumption.
AKKOMA, FEDIBIRD, HOMETOWN, ICESHRIMP, PIXELFED, PLEROMA, UNKNOWN -> {
c[ORG_JOINMASTODON_FILTERS_SERVER] = "1.0.0".toVersion()
c[ORG_JOINMASTODON_STATUSES_SCHEDULED] = "1.0.0".toVersion()
}
}
return c

View File

@ -41,6 +41,7 @@ import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_LI
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_SCHEDULED
import app.pachli.core.model.ServerOperation.ORG_JOINMASTODON_STATUSES_TRANSLATE
import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Configuration
@ -148,6 +149,7 @@ class ServerTest(
ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(),
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
),
),
),
@ -170,6 +172,7 @@ class ServerTest(
ORG_JOINMASTODON_FILTERS_CLIENT to "1.1.0".toVersion(),
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_FROM to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.0.0".toVersion(),
),
),
@ -205,6 +208,7 @@ class ServerTest(
ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY to "1.0.0".toVersion(),
ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_TRANSLATE to "1.1.0".toVersion(),
),
),
@ -212,7 +216,7 @@ class ServerTest(
),
arrayOf(
Triple(
"GoToSocial has no translation or filtering",
"GoToSocial has no translation, filtering, or scheduling",
NodeInfo.Software("gotosocial", "0.13.1 git-ccecf5a"),
defaultInstance,
),
@ -260,7 +264,7 @@ class ServerTest(
),
arrayOf(
Triple(
"Pleroma can filter",
"Pleroma can filter, schedule",
NodeInfo.Software("pleroma", "2.6.50-875-g2eb5c453.service-origin+soapbox"),
defaultInstance,
),
@ -270,13 +274,14 @@ class ServerTest(
version = "2.6.50-875-g2eb5c453.service-origin+soapbox".toVersion(),
capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
),
),
),
),
arrayOf(
Triple(
"Akkoma can filter",
"Akkoma can filter, schedule",
NodeInfo.Software("akkoma", "3.9.3-0-gd83f5f66f-blob"),
defaultInstance,
),
@ -286,6 +291,7 @@ class ServerTest(
version = "3.9.3-0-gd83f5f66f-blob".toVersion(),
capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
),
),
),
@ -306,7 +312,7 @@ class ServerTest(
),
arrayOf(
Triple(
"Friendica can filter",
"Friendica can filter, schedule",
NodeInfo.Software("friendica", "2023.05-1542"),
defaultInstance,
),
@ -316,6 +322,7 @@ class ServerTest(
version = "2023.5.0".toVersion(),
capabilities = mapOf(
ORG_JOINMASTODON_FILTERS_SERVER to "1.0.0".toVersion(),
ORG_JOINMASTODON_STATUSES_SCHEDULED to "1.0.0".toVersion(),
),
),
),