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:
parent
8fac5c3d4d
commit
c8aa4fd374
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue