|
|
|
|
@ -33,20 +33,18 @@ import com.keylesspalace.tusky.entity.Emoji
|
|
|
|
|
import com.keylesspalace.tusky.entity.NewPoll |
|
|
|
|
import com.keylesspalace.tusky.entity.Status |
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi |
|
|
|
|
import com.keylesspalace.tusky.service.MediaToSend |
|
|
|
|
import com.keylesspalace.tusky.service.ServiceClient |
|
|
|
|
import com.keylesspalace.tusky.service.StatusToSend |
|
|
|
|
import com.keylesspalace.tusky.util.randomAlphanumericString |
|
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
|
import kotlinx.coroutines.FlowPreview |
|
|
|
|
import kotlinx.coroutines.Job |
|
|
|
|
import kotlinx.coroutines.channels.BufferOverflow |
|
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow |
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow |
|
|
|
|
import kotlinx.coroutines.flow.SharedFlow |
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted |
|
|
|
|
import kotlinx.coroutines.flow.asFlow |
|
|
|
|
import kotlinx.coroutines.flow.catch |
|
|
|
|
import kotlinx.coroutines.flow.filter |
|
|
|
|
import kotlinx.coroutines.flow.first |
|
|
|
|
import kotlinx.coroutines.flow.shareIn |
|
|
|
|
import kotlinx.coroutines.flow.update |
|
|
|
|
@ -97,8 +95,6 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList()) |
|
|
|
|
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) |
|
|
|
|
|
|
|
|
|
private val mediaToJob = mutableMapOf<Int, Job>() |
|
|
|
|
|
|
|
|
|
// Used in ComposeActivity to pass state to result function when cropImage contract inflight |
|
|
|
|
var cropImageItemOld: QueuedMedia? = null |
|
|
|
|
|
|
|
|
|
@ -134,17 +130,18 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
|
|
|
|
|
media.updateAndGet { mediaValue -> |
|
|
|
|
val mediaItem = QueuedMedia( |
|
|
|
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, |
|
|
|
|
localId = mediaUploader.getNewLocalMediaId(), |
|
|
|
|
uri = uri, |
|
|
|
|
type = type, |
|
|
|
|
mediaSize = mediaSize, |
|
|
|
|
description = description, |
|
|
|
|
focus = focus |
|
|
|
|
focus = focus, |
|
|
|
|
state = QueuedMedia.State.UPLOADING |
|
|
|
|
) |
|
|
|
|
stashMediaItem = mediaItem |
|
|
|
|
|
|
|
|
|
if (replaceItem != null) { |
|
|
|
|
mediaToJob[replaceItem.localId]?.cancel() |
|
|
|
|
mediaUploader.cancelUploadScope(replaceItem.localId) |
|
|
|
|
mediaValue.map { |
|
|
|
|
if (it.localId == replaceItem.localId) mediaItem else it |
|
|
|
|
} |
|
|
|
|
@ -154,13 +151,9 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
} |
|
|
|
|
val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that |
|
|
|
|
|
|
|
|
|
mediaToJob[mediaItem.localId] = viewModelScope.launch { |
|
|
|
|
viewModelScope.launch { |
|
|
|
|
mediaUploader |
|
|
|
|
.uploadMedia(mediaItem, instanceInfo.first()) |
|
|
|
|
.catch { error -> |
|
|
|
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } } |
|
|
|
|
uploadError.emit(error) |
|
|
|
|
} |
|
|
|
|
.collect { event -> |
|
|
|
|
val item = media.value.find { it.localId == mediaItem.localId } |
|
|
|
|
?: return@collect |
|
|
|
|
@ -168,7 +161,16 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
is UploadEvent.ProgressEvent -> |
|
|
|
|
item.copy(uploadPercent = event.percentage) |
|
|
|
|
is UploadEvent.FinishedEvent -> |
|
|
|
|
item.copy(id = event.mediaId, uploadPercent = -1) |
|
|
|
|
item.copy( |
|
|
|
|
id = event.mediaId, |
|
|
|
|
uploadPercent = -1, |
|
|
|
|
state = if (event.processed) { QueuedMedia.State.PROCESSED } else { QueuedMedia.State.UNPROCESSED } |
|
|
|
|
) |
|
|
|
|
is UploadEvent.ErrorEvent -> { |
|
|
|
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } } |
|
|
|
|
uploadError.emit(event.error) |
|
|
|
|
return@collect |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
media.update { mediaValue -> |
|
|
|
|
mediaValue.map { mediaItem -> |
|
|
|
|
@ -187,7 +189,7 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) { |
|
|
|
|
media.update { mediaValue -> |
|
|
|
|
val mediaItem = QueuedMedia( |
|
|
|
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, |
|
|
|
|
localId = mediaUploader.getNewLocalMediaId(), |
|
|
|
|
uri = uri, |
|
|
|
|
type = type, |
|
|
|
|
mediaSize = 0, |
|
|
|
|
@ -195,14 +197,14 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
id = id, |
|
|
|
|
description = description, |
|
|
|
|
focus = focus, |
|
|
|
|
processed = true, |
|
|
|
|
state = QueuedMedia.State.PUBLISHED |
|
|
|
|
) |
|
|
|
|
mediaValue + mediaItem |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun removeMediaFromQueue(item: QueuedMedia) { |
|
|
|
|
mediaToJob[item.localId]?.cancel() |
|
|
|
|
mediaUploader.cancelUploadScope(item.localId) |
|
|
|
|
media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -240,6 +242,10 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun stopUploads() { |
|
|
|
|
mediaUploader.cancelUploadScope(*media.value.map { it.localId }.toIntArray()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun shouldShowSaveDraftDialog(): Boolean { |
|
|
|
|
// if any of the media files need to be downloaded first it could take a while, so show a loading dialog |
|
|
|
|
return media.value.any { mediaValue -> |
|
|
|
|
@ -289,47 +295,36 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
api.deleteScheduledStatus(scheduledTootId!!) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
media |
|
|
|
|
.filter { items -> items.all { it.uploadPercent == -1 } } |
|
|
|
|
.first { |
|
|
|
|
val mediaIds: MutableList<String> = mutableListOf() |
|
|
|
|
val mediaUris: MutableList<Uri> = mutableListOf() |
|
|
|
|
val mediaDescriptions: MutableList<String> = mutableListOf() |
|
|
|
|
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf() |
|
|
|
|
val mediaProcessed: MutableList<Boolean> = mutableListOf() |
|
|
|
|
media.value.forEach { item -> |
|
|
|
|
mediaIds.add(item.id!!) |
|
|
|
|
mediaUris.add(item.uri) |
|
|
|
|
mediaDescriptions.add(item.description ?: "") |
|
|
|
|
mediaFocus.add(item.focus) |
|
|
|
|
mediaProcessed.add(item.processed) |
|
|
|
|
} |
|
|
|
|
val tootToSend = StatusToSend( |
|
|
|
|
text = content, |
|
|
|
|
warningText = spoilerText, |
|
|
|
|
visibility = statusVisibility.value.serverString(), |
|
|
|
|
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), |
|
|
|
|
mediaIds = mediaIds, |
|
|
|
|
mediaUris = mediaUris.map { it.toString() }, |
|
|
|
|
mediaDescriptions = mediaDescriptions, |
|
|
|
|
mediaFocus = mediaFocus, |
|
|
|
|
scheduledAt = scheduledAt.value, |
|
|
|
|
inReplyToId = inReplyToId, |
|
|
|
|
poll = poll.value, |
|
|
|
|
replyingStatusContent = null, |
|
|
|
|
replyingStatusAuthorUsername = null, |
|
|
|
|
accountId = accountManager.activeAccount!!.id, |
|
|
|
|
draftId = draftId, |
|
|
|
|
idempotencyKey = randomAlphanumericString(16), |
|
|
|
|
retries = 0, |
|
|
|
|
mediaProcessed = mediaProcessed, |
|
|
|
|
language = postLanguage, |
|
|
|
|
statusId = originalStatusId, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
serviceClient.sendToot(tootToSend) |
|
|
|
|
true |
|
|
|
|
} |
|
|
|
|
val attachedMedia = media.value.map { item -> |
|
|
|
|
MediaToSend( |
|
|
|
|
localId = item.localId, |
|
|
|
|
id = item.id, |
|
|
|
|
uri = item.uri.toString(), |
|
|
|
|
description = item.description, |
|
|
|
|
focus = item.focus, |
|
|
|
|
processed = item.state == QueuedMedia.State.PROCESSED || item.state == QueuedMedia.State.PUBLISHED |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
val tootToSend = StatusToSend( |
|
|
|
|
text = content, |
|
|
|
|
warningText = spoilerText, |
|
|
|
|
visibility = statusVisibility.value.serverString(), |
|
|
|
|
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), |
|
|
|
|
media = attachedMedia, |
|
|
|
|
scheduledAt = scheduledAt.value, |
|
|
|
|
inReplyToId = inReplyToId, |
|
|
|
|
poll = poll.value, |
|
|
|
|
replyingStatusContent = null, |
|
|
|
|
replyingStatusAuthorUsername = null, |
|
|
|
|
accountId = accountManager.activeAccount!!.id, |
|
|
|
|
draftId = draftId, |
|
|
|
|
idempotencyKey = randomAlphanumericString(16), |
|
|
|
|
retries = 0, |
|
|
|
|
language = postLanguage, |
|
|
|
|
statusId = originalStatusId |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
serviceClient.sendToot(tootToSend) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Updates a QueuedMedia item arbitrarily, then sends description and focus to server |
|
|
|
|
@ -360,15 +355,15 @@ class ComposeViewModel @Inject constructor(
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
suspend fun updateDescription(localId: Int, description: String): Boolean { |
|
|
|
|
return updateMediaItem(localId, { mediaItem -> |
|
|
|
|
return updateMediaItem(localId) { mediaItem -> |
|
|
|
|
mediaItem.copy(description = description) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
suspend fun updateFocus(localId: Int, focus: Attachment.Focus): Boolean { |
|
|
|
|
return updateMediaItem(localId, { mediaItem -> |
|
|
|
|
return updateMediaItem(localId) { mediaItem -> |
|
|
|
|
mediaItem.copy(focus = focus) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> { |
|
|
|
|
|