/* Copyright 2019 Tusky Contributors * * This file is a part of Tusky. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ package com.keylesspalace.tusky.components.compose import android.net.Uri import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.components.common.CommonComposeViewModel import com.keylesspalace.tusky.components.common.MediaUploader import com.keylesspalace.tusky.components.common.UploadEvent import com.keylesspalace.tusky.components.common.mutableLiveData import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.InstanceEntity import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.util.* import io.reactivex.Single import io.reactivex.disposables.Disposable import io.reactivex.rxkotlin.Singles import retrofit2.Response import java.util.* import javax.inject.Inject class ComposeViewModel @Inject constructor( private val api: MastodonApi, private val accountManager: AccountManager, private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, private val saveTootHelper: SaveTootHelper, private val db: AppDatabase ) : CommonComposeViewModel(api, accountManager, mediaUploader, db) { private var replyingStatusAuthor: String? = null private var replyingStatusContent: String? = null internal var startingText: String? = null private var savedTootUid: Int = 0 private var startingContentWarning: String = "" private var inReplyToId: String? = null private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var contentWarningStateChanged: Boolean = false public var formattingSyntax: String = "" val markMediaAsSensitive = mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) fun toggleMarkSensitive() { this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!! } val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val showContentWarning = mutableLiveData(false) val setupComplete = mutableLiveData(false) val poll: MutableLiveData = mutableLiveData(null) val scheduledAt: MutableLiveData = mutableLiveData(null) fun didChange(content: String?, contentWarning: String?): Boolean { val textChanged = !(content.isNullOrEmpty() || startingText?.startsWith(content.toString()) ?: false) val contentWarningChanged = showContentWarning.value!! && !contentWarning.isNullOrEmpty() && !startingContentWarning.startsWith(contentWarning.toString()) val mediaChanged = !media.value.isNullOrEmpty() val pollChanged = poll.value != null return textChanged || contentWarningChanged || mediaChanged || pollChanged } fun contentWarningChanged(value: Boolean) { showContentWarning.value = value contentWarningStateChanged = true } fun deleteDraft() { saveTootHelper.deleteDraft(this.savedTootUid) } fun saveDraft(content: String, contentWarning: String) { val mediaUris = mutableListOf() val mediaDescriptions = mutableListOf() for (item in media.value!!) { mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) } saveTootHelper.saveToot( content, contentWarning, null, mediaUris, mediaDescriptions, savedTootUid, inReplyToId, replyingStatusContent, replyingStatusAuthor, statusVisibility.value!!, poll.value, formattingSyntax ) } /** * Send status to the server. * Uses current state plus provided arguments. * @return LiveData which will signal once the screen can be closed or null if there are errors */ fun sendStatus( content: String, spoilerText: String, preview: Boolean ): LiveData { return media .filter { items -> items.all { it.uploadPercent == -1 } } .map { val mediaIds = ArrayList() val mediaUris = ArrayList() val mediaDescriptions = ArrayList() for (item in media.value!!) { mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") } val tootToSend = TootToSend( content, spoilerText, statusVisibility.value!!.serverString(), mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), mediaIds, mediaUris.map { it.toString() }, mediaDescriptions, scheduledAt = scheduledAt.value, inReplyToId = inReplyToId, poll = poll.value, replyingStatusContent = null, replyingStatusAuthorUsername = null, formattingSyntax = formattingSyntax, preview = preview, savedJsonUrls = null, accountId = accountManager.activeAccount!!.id, savedTootUid = 0, idempotencyKey = randomAlphanumericString(16), retries = 0 ) serviceClient.sendToot(tootToSend) } } override fun onCleared() { for (uploadDisposable in mediaToDisposable.values) { uploadDisposable.dispose() } super.onCleared() } fun setup(composeOptions: ComposeActivity.ComposeOptions?) { super.setup() val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN startingVisibility = Status.Visibility.byNum( preferredVisibility.num.coerceAtLeast(replyVisibility.num)) inReplyToId = composeOptions?.inReplyToId val contentWarning = composeOptions?.contentWarning if (contentWarning != null) { startingContentWarning = contentWarning } if (!contentWarningStateChanged) { showContentWarning.value = !contentWarning.isNullOrBlank() } // recreate media list // when coming from SavedTootActivity val loadedDraftMediaUris = composeOptions?.mediaUrls val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) .forEach { (uri, description) -> pickMedia(uri.toUri(), null).observeForever { errorOrItem -> if (errorOrItem.isRight() && description != null) { updateDescription(errorOrItem.asRight().localId, description) } } } } else composeOptions?.mediaAttachments?.forEach { a -> // when coming from redraft val mediaType = when (a.type) { Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO } addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) } savedTootUid = composeOptions?.savedTootUid ?: 0 startingText = composeOptions?.tootText val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { startingVisibility = tootVisibility } statusVisibility.value = startingVisibility val mentionedUsernames = composeOptions?.mentionedUsernames if (mentionedUsernames != null) { val builder = StringBuilder() for (name in mentionedUsernames) { builder.append('@') builder.append(name) builder.append(' ') } startingText = builder.toString() } scheduledAt.value = composeOptions?.scheduledAt composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } val poll = composeOptions?.poll if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) { this.poll.value = poll } replyingStatusContent = composeOptions?.replyingStatusContent replyingStatusAuthor = composeOptions?.replyingStatusAuthor formattingSyntax = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax } fun updatePoll(newPoll: NewPoll) { poll.value = newPoll } fun updateScheduledAt(newScheduledAt: String?) { scheduledAt.value = newScheduledAt } private companion object { const val TAG = "ComposeViewModel" } }