From c3c92fadcd905ee0d283637a8e061eb1bed3b6c3 Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Tue, 23 Jun 2020 18:18:41 +0300 Subject: [PATCH] ComposeActivity: preview ability for Pleroma, WIP --- .../keylesspalace/tusky/appstore/Events.kt | 1 + .../components/compose/ComposeActivity.kt | 54 ++++++++++++-- .../components/compose/ComposeViewModel.kt | 4 +- .../keylesspalace/tusky/entity/Instance.kt | 11 ++- .../keylesspalace/tusky/entity/NewStatus.kt | 3 +- .../receiver/SendStatusBroadcastReceiver.kt | 1 + .../tusky/service/SendTootService.kt | 12 ++-- .../keylesspalace/tusky/view/StatusView.kt | 71 +++++++++++++++++++ app/src/main/res/layout/activity_compose.xml | 38 +++++++++- 9 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt 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 f5f4fd6f..61d1cd13 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -14,6 +14,7 @@ data class UnfollowEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable data class StatusDeletedEvent(val statusId: String) : Dispatchable +data class StatusPreviewEvent(val status: Status) : Dispatchable data class StatusComposedEvent(val status: Status) : Dispatchable data class StatusScheduledEvent(val status: Status) : Dispatchable data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable 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 305a732a..d23ed9b8 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 @@ -70,6 +70,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener @@ -108,12 +109,16 @@ class ComposeActivity : BaseActivity(), @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 stickerBehavior: BottomSheetBehavior<*> + private lateinit var previewBehavior: BottomSheetBehavior<*> // this only exists when a status is trying to be sent, but uploads are still occurring private var finishingUploadDialog: ProgressDialog? = null @@ -192,6 +197,12 @@ class ComposeActivity : BaseActivity(), viewModel.setupComplete.value = true stickerKeyboard.isSticky = true + + eventHub.events.subscribe { event -> + when(event) { + is StatusPreviewEvent -> onStatusPreviewReady(event.status) + } + } } private fun uriToFilename(uri: Uri): String { @@ -383,6 +394,7 @@ class ComposeActivity : BaseActivity(), } if(instanceData.software.equals("pleroma")) { + composePreviewButton.visibility = View.VISIBLE reenableAttachments() } } @@ -455,13 +467,15 @@ class ComposeActivity : BaseActivity(), scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) emojiBehavior = BottomSheetBehavior.from(emojiView) stickerBehavior = BottomSheetBehavior.from(stickerKeyboard) + previewBehavior = BottomSheetBehavior.from(previewScroll) emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false) enableButton(composeEmojiButton, clickable = false, colorActive = false) enableButton(composeStickerButton, false, false) // Setup the interface buttons. - composeTootButton.setOnClickListener { onSendClicked() } + composeTootButton.setOnClickListener { onSendClicked(false) } + composePreviewButton.setOnClickListener { onSendClicked(true) } composeAddMediaButton.setOnClickListener { openPickDialog() } composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } composeContentWarningButton.setOnClickListener { onContentWarningChanged() } @@ -771,6 +785,7 @@ class ComposeActivity : BaseActivity(), composeScheduleButton.isClickable = enable composeFormattingSyntax.isClickable = enable composeTootButton.isEnabled = enable + composePreviewButton.isEnabled = enable composeStickerButton.isEnabled = enable } @@ -796,6 +811,7 @@ class ComposeActivity : BaseActivity(), emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -816,6 +832,7 @@ class ComposeActivity : BaseActivity(), addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -833,6 +850,7 @@ class ComposeActivity : BaseActivity(), composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -847,6 +865,7 @@ class ComposeActivity : BaseActivity(), emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -947,13 +966,31 @@ class ComposeActivity : BaseActivity(), return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value)) } - private fun onSendClicked() { + private fun onSendClicked(preview: Boolean) { + if(preview && previewBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN + return + } + if (verifyScheduledTime()) { - sendStatus() + sendStatus(preview) } else { showScheduleView() } } + + private fun onStatusPreviewReady(status: Status) { + enableButtons(true) + 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 + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + + // Log.d("ComposeActivityPreview", "Preview: " + status.content) + } /** This is for the fancy keyboards which can insert images and stuff. */ override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { @@ -977,7 +1014,7 @@ class ComposeActivity : BaseActivity(), return false } - private fun sendStatus() { + private fun sendStatus(preview: Boolean) { enableButtons(false) val contentText = composeEditField.text.toString() var spoilerText = "" @@ -993,9 +1030,10 @@ class ComposeActivity : BaseActivity(), this, getString(R.string.dialog_title_finishing_media_upload), getString(R.string.dialog_message_uploading_media), true, true) - viewModel.sendStatus(contentText, spoilerText).observe(this, Observer { + viewModel.sendStatus(contentText, spoilerText, preview).observe(this, Observer { finishingUploadDialog?.dismiss() - deleteDraftAndFinish() + if(!preview) + deleteDraftAndFinish() }) } else { @@ -1156,6 +1194,7 @@ class ComposeActivity : BaseActivity(), emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN return } @@ -1168,7 +1207,7 @@ class ComposeActivity : BaseActivity(), if (event.isCtrlPressed) { if (keyCode == KeyEvent.KEYCODE_ENTER) { // send toot by pressing CTRL + ENTER - this.onSendClicked() + this.onSendClicked(false) return true } } @@ -1229,6 +1268,7 @@ class ComposeActivity : BaseActivity(), composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + previewBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN } 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 4311ab2f..f0b0a8ed 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 @@ -302,7 +302,8 @@ class ComposeViewModel */ fun sendStatus( content: String, - spoilerText: String + spoilerText: String, + preview: Boolean ): LiveData { return media .filter { items -> items.all { it.uploadPercent == -1 } } @@ -330,6 +331,7 @@ class ComposeViewModel replyingStatusContent = null, replyingStatusAuthorUsername = null, formattingSyntax = formattingSyntax, + preview = preview, savedJsonUrls = null, accountId = accountManager.activeAccount!!.id, savedTootUid = 0, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index a9f6f499..fb3b0559 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -30,7 +30,8 @@ data class Instance ( @SerializedName("contact_account") val contactAccount: Account, @SerializedName("max_toot_chars") val maxTootChars: Int?, @SerializedName("max_bio_chars") val maxBioChars: Int?, - @SerializedName("poll_limits") val pollLimits: PollLimits? + @SerializedName("poll_limits") val pollLimits: PollLimits?, + val pleroma: InstancePleroma ) { override fun hashCode(): Int { return uri.hashCode() @@ -45,6 +46,14 @@ data class Instance ( } } +data class InstancePleroma ( + val metadata: InstancePleromaMetadata +) + +data class InstancePleromaMetadata ( + val features: List +) + data class PollLimits ( @SerializedName("max_options") val maxOptions: Int?, @SerializedName("max_option_chars") val maxOptionChars: Int? 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 b9d9278b..5f64db12 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -28,7 +28,8 @@ data class NewStatus( @SerializedName("media_ids") val mediaIds: List?, @SerializedName("scheduled_at") val scheduledAt: String?, val poll: NewPoll?, - var content_type: String? + var content_type: 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 fe367039..154d1160 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -103,6 +103,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { null, null, "", + false, account.id, 0, randomAlphanumericString(16), diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index 7748fbcd..9dd6bba1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -16,8 +16,7 @@ import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.appstore.StatusComposedEvent -import com.keylesspalace.tusky.appstore.StatusScheduledEvent +import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase @@ -132,6 +131,7 @@ class SendTootService : Service(), Injectable { tootToSend.retries++ val contentType : String? = if(tootToSend.formattingSyntax.length == 0) null else tootToSend.formattingSyntax + val preview : Boolean? = if(tootToSend.preview) tootToSend.preview else null val newStatus = NewStatus( tootToSend.text, @@ -142,7 +142,8 @@ class SendTootService : Service(), Injectable { tootToSend.mediaIds, tootToSend.scheduledAt, tootToSend.poll, - contentType + contentType, + preview ) val sendCall = mastodonApi.createStatus( @@ -166,7 +167,9 @@ class SendTootService : Service(), Injectable { saveTootHelper.deleteDraft(tootToSend.savedTootUid) } - if (scheduled) { + if (tootToSend.preview) { + response.body()?.let(::StatusPreviewEvent)?.let(eventHub::dispatch) + } else if (scheduled) { response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) } else { response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch) @@ -328,6 +331,7 @@ data class TootToSend( val replyingStatusAuthorUsername: String?, val savedJsonUrls: List?, val formattingSyntax: String, + val preview: Boolean, val accountId: Long, val savedTootUid: Int, val idempotencyKey: String, 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..1fc4a2e7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/StatusView.kt @@ -0,0 +1,71 @@ +package com.keylesspalace.tusky.view + +import android.view.* +import android.content.* +import android.util.* +import android.widget.* +import android.app.* +import android.text.* +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator + +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.ViewDataUtils +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.R + +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("animateGifAvatars", false), + mediaPreviewEnabled = true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = false, + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true) + ) + viewHolder = StatusViewHolder(this) + } + + fun setupWithStatus(status: Status) { + val concrete = ViewDataUtils.statusToViewData(status, 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 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) { } + } +} diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 079f28a2..aab009ac 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -301,10 +301,29 @@ android:layout_height="300dp" android:background="?attr/colorSurface" android:elevation="12dp" - android:layout_marginBottom="56dp" + android:paddingBottom="60dp" app:behavior_hideable="true" app:behavior_peekHeight="0dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> + + + + + +