Add post editing capability (#2828)
* Add post editing capability * Don't try to reprocess already uploaded attachments. Fixes editing posts with existing media * Don't mark post edits as modified until editing occurs * Disable UI for things that can't be edited when editing a post * Finally convert SFragment to kotlin * Use api endpoint for fetching status source for editing * Apply review feedback
This commit is contained in:
parent
51d02388b9
commit
a6b6a40ba6
20 changed files with 676 additions and 527 deletions
|
@ -238,15 +238,14 @@ class ComposeActivity :
|
|||
binding.composeMediaPreviewBar.adapter = mediaAdapter
|
||||
binding.composeMediaPreviewBar.itemAnimator = null
|
||||
|
||||
setupButtons()
|
||||
subscribeToUpdates(mediaAdapter)
|
||||
|
||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||
* based on what the intent from the reply request passes. */
|
||||
val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
||||
|
||||
viewModel.setup(composeOptions)
|
||||
|
||||
setupButtons()
|
||||
subscribeToUpdates(mediaAdapter)
|
||||
|
||||
if (accountManager.shouldDisplaySelfUsername(this)) {
|
||||
binding.composeUsernameView.text = getString(
|
||||
R.string.compose_active_account_description,
|
||||
|
@ -708,20 +707,25 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun updateScheduleButton() {
|
||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
if (viewModel.editing) {
|
||||
// Can't reschedule a published status
|
||||
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
||||
} else {
|
||||
getColor(R.color.tusky_blue)
|
||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
} else {
|
||||
getColor(R.color.tusky_blue)
|
||||
}
|
||||
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
private fun enableButtons(enable: Boolean) {
|
||||
private fun enableButtons(enable: Boolean, editing: Boolean) {
|
||||
binding.composeAddMediaButton.isClickable = enable
|
||||
binding.composeToggleVisibilityButton.isClickable = enable
|
||||
binding.composeToggleVisibilityButton.isClickable = enable && !editing
|
||||
binding.composeEmojiButton.isClickable = enable
|
||||
binding.composeHideMediaButton.isClickable = enable
|
||||
binding.composeScheduleButton.isClickable = enable
|
||||
binding.composeScheduleButton.isClickable = enable && !editing
|
||||
binding.composeTootButton.isEnabled = enable
|
||||
}
|
||||
|
||||
|
@ -737,6 +741,10 @@ class ComposeActivity :
|
|||
else -> R.drawable.ic_lock_open_24dp
|
||||
}
|
||||
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
||||
if (viewModel.editing) {
|
||||
// Can't update visibility on published status
|
||||
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showComposeOptions() {
|
||||
|
@ -938,7 +946,7 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun sendStatus() {
|
||||
enableButtons(false)
|
||||
enableButtons(false, viewModel.editing)
|
||||
val contentText = binding.composeEditField.text.toString()
|
||||
var spoilerText = ""
|
||||
if (viewModel.showContentWarning.value) {
|
||||
|
@ -947,7 +955,7 @@ class ComposeActivity :
|
|||
val characterCount = calculateTextLength()
|
||||
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||
binding.composeEditField.error = getString(R.string.error_empty)
|
||||
enableButtons(true)
|
||||
enableButtons(true, viewModel.editing)
|
||||
} else if (characterCount <= maximumTootCharacters) {
|
||||
if (viewModel.media.value.isNotEmpty()) {
|
||||
finishingUploadDialog = ProgressDialog.show(
|
||||
|
@ -963,7 +971,7 @@ class ComposeActivity :
|
|||
}
|
||||
} else {
|
||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
enableButtons(true)
|
||||
enableButtons(true, viewModel.editing)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1179,7 +1187,8 @@ class ComposeActivity :
|
|||
val uploadPercent: Int = 0,
|
||||
val id: String? = null,
|
||||
val description: String? = null,
|
||||
val focus: Attachment.Focus? = null
|
||||
val focus: Attachment.Focus? = null,
|
||||
val processed: Boolean = false,
|
||||
) {
|
||||
enum class Type {
|
||||
IMAGE, VIDEO, AUDIO;
|
||||
|
@ -1230,6 +1239,7 @@ class ComposeActivity :
|
|||
var poll: NewPoll? = null,
|
||||
var modifiedInitialState: Boolean? = null,
|
||||
var language: String? = null,
|
||||
var statusId: String? = null,
|
||||
) : Parcelable
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -73,6 +73,7 @@ class ComposeViewModel @Inject constructor(
|
|||
private var scheduledTootId: String? = null
|
||||
private var startingContentWarning: String = ""
|
||||
private var inReplyToId: String? = null
|
||||
private var originalStatusId: String? = null
|
||||
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||
|
||||
private var contentWarningStateChanged: Boolean = false
|
||||
|
@ -193,7 +194,8 @@ class ComposeViewModel @Inject constructor(
|
|||
uploadPercent = -1,
|
||||
id = id,
|
||||
description = description,
|
||||
focus = focus
|
||||
focus = focus,
|
||||
processed = true,
|
||||
)
|
||||
mediaValue + mediaItem
|
||||
}
|
||||
|
@ -270,6 +272,7 @@ class ComposeViewModel @Inject constructor(
|
|||
failedToSend = false,
|
||||
scheduledAt = scheduledAt.value,
|
||||
language = postLanguage,
|
||||
statusId = originalStatusId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -299,7 +302,7 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaUris.add(item.uri)
|
||||
mediaDescriptions.add(item.description ?: "")
|
||||
mediaFocus.add(item.focus)
|
||||
mediaProcessed.add(false)
|
||||
mediaProcessed.add(item.processed)
|
||||
}
|
||||
val tootToSend = StatusToSend(
|
||||
text = content,
|
||||
|
@ -321,6 +324,7 @@ class ComposeViewModel @Inject constructor(
|
|||
retries = 0,
|
||||
mediaProcessed = mediaProcessed,
|
||||
language = postLanguage,
|
||||
statusId = originalStatusId,
|
||||
)
|
||||
|
||||
serviceClient.sendToot(tootToSend)
|
||||
|
@ -452,6 +456,7 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
draftId = composeOptions?.draftId ?: 0
|
||||
scheduledTootId = composeOptions?.scheduledTootId
|
||||
originalStatusId = composeOptions?.statusId
|
||||
startingText = composeOptions?.content
|
||||
postLanguage = composeOptions?.language
|
||||
|
||||
|
@ -497,6 +502,9 @@ class ComposeViewModel @Inject constructor(
|
|||
scheduledAt.value = newScheduledAt
|
||||
}
|
||||
|
||||
val editing: Boolean
|
||||
get() = !originalStatusId.isNullOrEmpty()
|
||||
|
||||
private companion object {
|
||||
const val TAG = "ComposeViewModel"
|
||||
}
|
||||
|
|
|
@ -48,10 +48,13 @@ class MediaPreviewAdapter(
|
|||
val addFocusId = 2
|
||||
val editImageId = 3
|
||||
val removeId = 4
|
||||
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) {
|
||||
popup.menu.add(0, addFocusId, 0, R.string.action_set_focus)
|
||||
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
||||
if (!item.processed) {
|
||||
// Already-published items can't have their metadata edited
|
||||
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||
if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) {
|
||||
popup.menu.add(0, addFocusId, 0, R.string.action_set_focus)
|
||||
popup.menu.add(0, editImageId, 0, R.string.action_edit_image)
|
||||
}
|
||||
}
|
||||
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
|
|
|
@ -65,6 +65,7 @@ class DraftHelper @Inject constructor(
|
|||
failedToSend: Boolean,
|
||||
scheduledAt: String?,
|
||||
language: String?,
|
||||
statusId: String?,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||
|
||||
|
@ -124,6 +125,7 @@ class DraftHelper @Inject constructor(
|
|||
failedToSend = failedToSend,
|
||||
scheduledAt = scheduledAt,
|
||||
language = language,
|
||||
statusId = statusId,
|
||||
)
|
||||
|
||||
draftDao.insertOrReplace(draft)
|
||||
|
|
|
@ -111,6 +111,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
visibility = draft.visibility,
|
||||
scheduledAt = draft.scheduledAt,
|
||||
language = draft.language,
|
||||
statusId = draft.statusId,
|
||||
)
|
||||
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
@ -147,6 +148,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
visibility = draft.visibility,
|
||||
scheduledAt = draft.scheduledAt,
|
||||
language = draft.language,
|
||||
statusId = draft.statusId,
|
||||
)
|
||||
|
||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
|||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -38,6 +39,9 @@ abstract class SearchFragment<T : Any> :
|
|||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
|
||||
protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
|
||||
|
||||
protected val binding by viewBinding(FragmentSearchBinding::bind)
|
||||
|
|
|
@ -33,13 +33,16 @@ import androidx.appcompat.widget.PopupMenu
|
|||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import at.connyduck.calladapter.networkresult.fold
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
|
@ -62,6 +65,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
|||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener {
|
||||
|
||||
|
@ -351,6 +355,10 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
showConfirmEditDialog(id, position, status)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.status_edit -> {
|
||||
editStatus(id, position, status)
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
R.id.pin -> {
|
||||
viewModel.pinAccount(status, !status.isPinned())
|
||||
return@setOnMenuItemClickListener true
|
||||
|
@ -487,4 +495,32 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
|||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun editStatus(id: String, position: Int, status: Status) {
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.statusSource(id).fold(
|
||||
{ source ->
|
||||
val composeOptions = ComposeOptions(
|
||||
content = source.text,
|
||||
inReplyToId = status.inReplyToId,
|
||||
visibility = status.visibility,
|
||||
contentWarning = source.spoilerText,
|
||||
mediaAttachments = status.attachments,
|
||||
sensitive = status.sensitive,
|
||||
language = status.language,
|
||||
statusId = source.id,
|
||||
poll = status.poll?.toNewPoll(status.createdAt),
|
||||
)
|
||||
startActivity(ComposeActivity.startIntent(requireContext(), composeOptions))
|
||||
},
|
||||
{
|
||||
Snackbar.make(
|
||||
requireView(),
|
||||
getString(R.string.error_status_source_load),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewM
|
|||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -85,9 +84,6 @@ class TimelineFragment :
|
|||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
private val viewModel: TimelineViewModel by lazy {
|
||||
if (kind == TimelineViewModel.Kind.HOME) {
|
||||
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue