diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 7b9e192a..cf59d6f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -164,6 +164,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd List descriptions = gson.fromJson(item.getDescriptions(), stringListType); ComposeOptions composeOptions = new ComposeOptions( + /*scheduledTootUid*/null, item.getUid(), item.getText(), jsonUrls, 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 ec3ba841..4d9d535d 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 @@ -1007,9 +1007,11 @@ class ComposeActivity : BaseActivity(), composeEditField.error = getString(R.string.error_empty) enableButtons(true) } else if (characterCount <= maximumTootCharacters) { - finishingUploadDialog = ProgressDialog.show( - this, getString(R.string.dialog_title_finishing_media_upload), - getString(R.string.dialog_message_uploading_media), true, true) + if (viewModel.media.value!!.isNotEmpty()) { + finishingUploadDialog = ProgressDialog.show( + this, getString(R.string.dialog_title_finishing_media_upload), + getString(R.string.dialog_message_uploading_media), true, true) + } viewModel.sendStatus(contentText, spoilerText, preview).observeOnce(this) { finishingUploadDialog?.dismiss() @@ -1311,6 +1313,7 @@ class ComposeActivity : BaseActivity(), @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin + var scheduledTootUid: String? = null, var savedTootUid: Int? = null, var tootText: String? = null, var mediaUrls: List? = null, 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 a0457f68..05f067ba 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 @@ -19,8 +19,10 @@ import android.net.Uri import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer +import androidx.work.impl.utils.LiveDataUtils import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.components.common.CommonComposeViewModel import com.keylesspalace.tusky.components.common.MediaUploader @@ -38,6 +40,8 @@ import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.util.* import io.reactivex.Single +import io.reactivex.Observable.empty +import io.reactivex.Observable.just import io.reactivex.disposables.Disposable import io.reactivex.rxkotlin.Singles import retrofit2.Response @@ -58,6 +62,7 @@ class ComposeViewModel private var replyingStatusContent: String? = null internal var startingText: String? = null private var savedTootUid: Int = 0 + private var scheduledTootUid: String? = null private var startingContentWarning: String = "" private var inReplyToId: String? = null private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN @@ -78,6 +83,105 @@ class ComposeViewModel val poll: MutableLiveData = mutableLiveData(null) val scheduledAt: MutableLiveData = mutableLiveData(null) + val media = mutableLiveData>(listOf()) + val uploadError = MutableLiveData() + + private val mediaToDisposable = mutableMapOf() + + private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() + + init { + + Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance -> + InstanceEntity( + instance = accountManager.activeAccount?.domain!!, + emojiList = emojis, + maximumTootCharacters = instance.maxTootChars, + maxPollOptions = instance.pollLimits?.maxOptions, + maxPollOptionLength = instance.pollLimits?.maxOptionChars, + version = instance.version + ) + } + .doOnSuccess { + db.instanceDao().insertOrReplace(it) + } + .onErrorResumeNext( + db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + ) + .subscribe ({ instanceEntity -> + emoji.postValue(instanceEntity.emojiList) + instance.postValue(instanceEntity) + }, { throwable -> + // this can happen on network error when no cached data is available + Log.w(TAG, "error loading instance data", throwable) + }) + .autoDispose() + } + + fun pickMedia(uri: Uri): LiveData> { + // We are not calling .toLiveData() here because we don't want to stop the process when + // the Activity goes away temporarily (like on screen rotation). + val liveData = MutableLiveData>() + mediaUploader.prepareMedia(uri) + .map { (type, uri, size) -> + val mediaItems = media.value!! + if (type != QueuedMedia.Type.IMAGE + && mediaItems.isNotEmpty() + && mediaItems[0].type == QueuedMedia.Type.IMAGE) { + throw VideoOrImageException() + } else { + addMediaToQueue(type, uri, size) + } + } + .subscribe({ queuedMedia -> + liveData.postValue(Either.Right(queuedMedia)) + }, { error -> + liveData.postValue(Either.Left(error)) + }) + .autoDispose() + return liveData + } + + private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia { + val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize) + media.value = media.value!! + mediaItem + mediaToDisposable[mediaItem.localId] = mediaUploader + .uploadMedia(mediaItem) + .subscribe ({ event -> + val item = media.value?.find { it.localId == mediaItem.localId } + ?: return@subscribe + val newMediaItem = when (event) { + is UploadEvent.ProgressEvent -> + item.copy(uploadPercent = event.percentage) + is UploadEvent.FinishedEvent -> + item.copy(id = event.attachment.id, uploadPercent = -1) + } + synchronized(media) { + val mediaValue = media.value!! + val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } + media.postValue(if (index == -1) { + mediaValue + newMediaItem + } else { + mediaValue.toMutableList().also { it[index] = newMediaItem } + }) + } + }, { error -> + media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) + uploadError.postValue(error) + }) + return mediaItem + } + + private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) { + val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description) + media.value = media.value!! + mediaItem + } + + fun removeMediaFromQueue(item: QueuedMedia) { + mediaToDisposable[item.localId]?.dispose() + media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } + } + fun didChange(content: String?, contentWarning: String?): Boolean { val textChanged = !(content.isNullOrEmpty() @@ -134,7 +238,14 @@ class ComposeViewModel spoilerText: String, preview: Boolean ): LiveData { - return media + + val deletionObservable = if (isEditingScheduledToot) { + api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } + } else { + just(Unit) + }.toLiveData() + + val sendObservable = media .filter { items -> items.all { it.uploadPercent == -1 } } .map { val mediaIds = ArrayList() @@ -167,8 +278,13 @@ class ComposeViewModel idempotencyKey = randomAlphanumericString(16), retries = 0 ) + serviceClient.sendToot(tootToSend) } + + return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } + + } override fun onCleared() { @@ -222,6 +338,7 @@ class ComposeViewModel savedTootUid = composeOptions?.savedTootUid ?: 0 + scheduledTootUid = composeOptions?.scheduledTootUid startingText = composeOptions?.tootText diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 2ade20b8..c5831fc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -124,6 +124,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( + scheduledTootUid = item.id, tootText = item.params.text, contentWarning = item.params.spoilerText, mediaAttachments = item.mediaAttachments,