From 6a6605621683cefc981cf04a30b019111a6bdf2f Mon Sep 17 00:00:00 2001 From: fruye Date: Sun, 13 Nov 2022 20:08:14 +0100 Subject: [PATCH] ComposeActivity: preview ability for Pleroma Based on these commits by a1ba: - c3c92fadcd905ee0d283637a8e061eb1bed3b6c3 - f9f2f9aa5bc9dee009cb9a343c8fe7c4534e29e5 --- .../keylesspalace/tusky/appstore/Events.kt | 1 + .../components/compose/ComposeActivity.kt | 50 ++++++++++-- .../components/compose/ComposeViewModel.kt | 6 +- .../keylesspalace/tusky/entity/NewStatus.kt | 3 +- .../receiver/SendStatusBroadcastReceiver.kt | 3 +- .../tusky/service/SendStatusService.kt | 11 ++- .../keylesspalace/tusky/view/StatusView.kt | 76 +++++++++++++++++++ app/src/main/res/drawable/ic_preview_24dp.xml | 9 +++ app/src/main/res/layout/activity_compose.xml | 34 ++++++++- 9 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt create mode 100644 app/src/main/res/drawable/ic_preview_24dp.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index c17d23ac..7768d727 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -16,6 +16,7 @@ data class StatusDeletedEvent(val statusId: String) : Event data class StatusComposedEvent(val status: Status) : Event data class StatusScheduledEvent(val status: Status) : Event data class StatusEditedEvent(val originalId: String, val status: Status) : Event +data class StatusPreviewEvent(val status: Status) : Event data class ProfileEditedEvent(val newProfileData: Account) : Event data class PreferenceChangedEvent(val preferenceKey: String) : Event data class MainTabsChangedEvent(val newTabs: List) : Event diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index c3476821..57bb10c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -78,6 +78,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.LocaleAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.components.compose.ComposeViewModel.ConfirmationKind import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog @@ -140,10 +141,14 @@ class ComposeActivity : @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var eventHub: EventHub + private lateinit var composeOptionsBehavior: BottomSheetBehavior<*> private lateinit var addMediaBehavior: BottomSheetBehavior<*> private lateinit var emojiBehavior: BottomSheetBehavior<*> private lateinit var scheduleBehavior: BottomSheetBehavior<*> + private lateinit var previewBehavior: BottomSheetBehavior<*> /** The account that is being used to compose the status */ private lateinit var activeAccount: AccountEntity @@ -427,6 +432,7 @@ class ComposeActivity : updateVisibleCharactersLeft() } } + binding.composePreviewButton.visibility = View.VISIBLE lifecycleScope.launch { viewModel.emoji.collect(::setEmojiList) @@ -489,6 +495,14 @@ class ComposeActivity : } } } + + lifecycleScope.launch { + eventHub.events.collect { event -> + when (event) { + is StatusPreviewEvent -> onStatusPreviewReady(event.status) + } + } + } } private fun setupButtons() { @@ -498,11 +512,13 @@ class ComposeActivity : addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet) scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) emojiBehavior = BottomSheetBehavior.from(binding.emojiView) + previewBehavior = BottomSheetBehavior.from(binding.previewScroll) enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) // Setup the interface buttons. - binding.composeTootButton.setOnClickListener { onSendClicked() } + binding.composeTootButton.setOnClickListener { onSendClicked(false) } + binding.composePreviewButton.setOnClickListener { onSendClicked(true) } binding.composeAddMediaButton.setOnClickListener { openPickDialog() } binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() } @@ -547,6 +563,7 @@ class ComposeActivity : addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN return } @@ -767,6 +784,7 @@ class ComposeActivity : addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) } @@ -786,6 +804,7 @@ class ComposeActivity : composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) } @@ -802,6 +821,7 @@ class ComposeActivity : composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) } @@ -815,6 +835,7 @@ class ComposeActivity : composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) } @@ -929,9 +950,13 @@ class ComposeActivity : return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)) } - private fun onSendClicked() { + private fun onSendClicked(preview: Boolean) { + if(preview && previewBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + if (verifyScheduledTime()) { - sendStatus() + sendStatus(preview) } else { showScheduleView() } @@ -954,7 +979,17 @@ class ComposeActivity : return contentInfo } - private fun sendStatus() { + private fun onStatusPreviewReady(status: Status) { + enableButtons(true, viewModel.editing) + binding.previewView.setupWithStatus(status) + previewBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + private fun sendStatus(preview: Boolean) { enableButtons(false, viewModel.editing) val contentText = binding.composeEditField.text.toString() var spoilerText = "" @@ -967,8 +1002,9 @@ class ComposeActivity : enableButtons(true, viewModel.editing) } else if (characterCount <= maximumTootCharacters) { lifecycleScope.launch { - viewModel.sendStatus(contentText, spoilerText, activeAccount.id) - deleteDraftAndFinish() + viewModel.sendStatus(contentText, spoilerText, activeAccount.id, preview) + if (!preview) + deleteDraftAndFinish() } } else { binding.composeEditField.error = getString(R.string.error_compose_character_limit) @@ -1114,7 +1150,7 @@ class ComposeActivity : if (event.isCtrlPressed) { if (keyCode == KeyEvent.KEYCODE_ENTER) { // send toot by pressing CTRL + ENTER - this.onSendClicked() + this.onSendClicked(false) return true } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 90389d28..c2ff5132 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -310,7 +310,8 @@ class ComposeViewModel @Inject constructor( suspend fun sendStatus( content: String, spoilerText: String, - accountId: Long + accountId: Long, + preview: Boolean ) { if (!scheduledTootId.isNullOrEmpty()) { api.deleteScheduledStatus(scheduledTootId!!) @@ -342,7 +343,8 @@ class ComposeViewModel @Inject constructor( idempotencyKey = randomAlphanumericString(16), retries = 0, language = postLanguage, - statusId = originalStatusId + statusId = originalStatusId, + preview = preview, ) serviceClient.sendToot(tootToSend) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt index 1a353ead..52080411 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -29,7 +29,8 @@ data class NewStatus( @SerializedName("media_attributes") val mediaAttributes: List?, @SerializedName("scheduled_at") val scheduledAt: String?, val poll: NewPoll?, - val language: String? + val language: String?, + val preview: Boolean?, ) @Parcelize diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 3ab941e9..a4b7c44d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -97,7 +97,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { idempotencyKey = randomAlphanumericString(16), retries = 0, language = null, - statusId = null + statusId = null, + preview = false, ) ) diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index cf03115b..d1898916 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -23,6 +23,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusComposedEvent import com.keylesspalace.tusky.appstore.StatusEditedEvent +import com.keylesspalace.tusky.appstore.StatusPreviewEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.MediaUploader import com.keylesspalace.tusky.components.compose.UploadEvent @@ -219,7 +220,8 @@ class SendStatusService : Service(), Injectable { focus = media.focus?.toMastodonApiString(), thumbnail = null ) - } + }, + preview = if (statusToSend.preview) true else null, ) val sendResult = if (isNew) { @@ -250,7 +252,9 @@ class SendStatusService : Service(), Injectable { val scheduled = !statusToSend.scheduledAt.isNullOrEmpty() - if (scheduled) { + if (statusToSend.preview) { + eventHub.dispatch(StatusPreviewEvent(sentStatus)) + } else if (scheduled) { eventHub.dispatch(StatusScheduledEvent(sentStatus)) } else if (!isNew) { eventHub.dispatch(StatusEditedEvent(statusToSend.statusId!!, sentStatus)) @@ -463,7 +467,8 @@ data class StatusToSend( val idempotencyKey: String, var retries: Int, val language: String?, - val statusId: String? + val statusId: String?, + val preview : Boolean, ) : Parcelable @Parcelize diff --git a/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt b/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt new file mode 100644 index 00000000..e79a17b5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt @@ -0,0 +1,76 @@ +package com.keylesspalace.tusky.view + +import android.view.* +import android.content.* +import android.util.* + +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.preference.PreferenceManager + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.viewdata.StatusViewData + +import java.util.*; + +class StatusView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : ConstraintLayout(context, attrs, defStyleAttr) { + + private var viewHolder : StatusViewHolder + private var statusDisplayOptions : StatusDisplayOptions + init { + View.inflate(context, R.layout.item_status, this) + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = true, // FIXME: get value from AccountManager + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = false, + confirmFavourites = false, + hideStats = false, + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = false, + showSensitiveMedia = false, // FIXME: get value from AccountManager + openSpoiler = false, // FIXME: get value from AccountManager + bigEmojis = preferences.getBoolean(PrefKeys.BIG_EMOJIS, true), + ) + viewHolder = StatusViewHolder(this) + } + + fun setupWithStatus(status: Status) { + val concrete = StatusViewData.Concrete(status, false, false, false) + viewHolder.setupWithStatus(concrete, DummyStatusActionListener(), statusDisplayOptions) + } + + class DummyStatusActionListener: StatusActionListener { + override fun onReply(position: Int) { } + override fun onReblog(reblog: Boolean, position: Int) { } + override fun onFavourite(favourite: Boolean, position: Int) { } + override fun onBookmark(bookmark: Boolean, position: Int) { } + override fun onMore(view: View, position: Int) { } + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { } + override fun onViewThread(position: Int) { } + override fun onViewReplyTo(position: Int) { } + override fun onOpenReblog(position: Int) { } + override fun onExpandedChange(expanded: Boolean, position: Int) { } + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { } + override fun onLoadMore(position: Int) { } + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { } + override fun onVoteInPoll(position: Int, choices: MutableList) { } + override fun onViewAccount(id: String) { } + override fun onViewTag(id: String) { } + override fun onViewUrl(id: String) { } + override fun clearWarningAction(position: Int) { } + } +} diff --git a/app/src/main/res/drawable/ic_preview_24dp.xml b/app/src/main/res/drawable/ic_preview_24dp.xml new file mode 100644 index 00000000..88992ac1 --- /dev/null +++ b/app/src/main/res/drawable/ic_preview_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 367d88ba..003152bf 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -278,6 +278,25 @@ app:behavior_peekHeight="0dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> + + + + + + + +