2019-12-19 19:09:40 +01:00
|
|
|
/* 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 <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.components.compose
|
|
|
|
|
|
|
|
import android.net.Uri
|
|
|
|
import android.util.Log
|
|
|
|
import androidx.core.net.toUri
|
2022-04-21 18:46:21 +02:00
|
|
|
import androidx.lifecycle.ViewModel
|
2021-06-24 21:23:29 +02:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2022-05-30 20:03:40 +02:00
|
|
|
import at.connyduck.calladapter.networkresult.fold
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
2022-05-17 19:55:37 +02:00
|
|
|
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
2021-01-21 18:57:09 +01:00
|
|
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
2022-04-21 18:46:21 +02:00
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.components.search.SearchType
|
|
|
|
import com.keylesspalace.tusky.db.AccountManager
|
2021-06-28 22:04:34 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment
|
|
|
|
import com.keylesspalace.tusky.entity.Emoji
|
|
|
|
import com.keylesspalace.tusky.entity.NewPoll
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Status
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
|
|
|
import com.keylesspalace.tusky.service.ServiceClient
|
2022-03-20 20:21:42 +01:00
|
|
|
import com.keylesspalace.tusky.service.StatusToSend
|
2021-06-28 22:04:34 +02:00
|
|
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
2022-04-21 18:46:21 +02:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.FlowPreview
|
2022-04-21 18:46:21 +02:00
|
|
|
import kotlinx.coroutines.Job
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.channels.BufferOverflow
|
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
2022-05-03 19:12:35 +02:00
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.flow.SharedFlow
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.asFlow
|
2022-04-21 18:46:21 +02:00
|
|
|
import kotlinx.coroutines.flow.catch
|
2022-05-03 19:12:35 +02:00
|
|
|
import kotlinx.coroutines.flow.filter
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.flow.first
|
|
|
|
import kotlinx.coroutines.flow.shareIn
|
2022-05-03 19:12:35 +02:00
|
|
|
import kotlinx.coroutines.flow.update
|
|
|
|
import kotlinx.coroutines.flow.updateAndGet
|
2021-06-24 21:23:29 +02:00
|
|
|
import kotlinx.coroutines.launch
|
2022-04-21 18:46:21 +02:00
|
|
|
import kotlinx.coroutines.withContext
|
2019-12-19 19:09:40 +01:00
|
|
|
import javax.inject.Inject
|
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
@OptIn(FlowPreview::class)
|
2021-01-21 18:57:09 +01:00
|
|
|
class ComposeViewModel @Inject constructor(
|
2021-06-28 22:04:34 +02:00
|
|
|
private val api: MastodonApi,
|
|
|
|
private val accountManager: AccountManager,
|
|
|
|
private val mediaUploader: MediaUploader,
|
|
|
|
private val serviceClient: ServiceClient,
|
|
|
|
private val draftHelper: DraftHelper,
|
2022-07-26 20:24:50 +02:00
|
|
|
instanceInfoRepo: InstanceInfoRepository
|
2022-04-21 18:46:21 +02:00
|
|
|
) : ViewModel() {
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
private var replyingStatusAuthor: String? = null
|
|
|
|
private var replyingStatusContent: String? = null
|
|
|
|
internal var startingText: String? = null
|
2021-01-21 18:57:09 +01:00
|
|
|
private var draftId: Int = 0
|
|
|
|
private var scheduledTootId: String? = null
|
2020-01-29 19:15:53 +01:00
|
|
|
private var startingContentWarning: String = ""
|
2019-12-19 19:09:40 +01:00
|
|
|
private var inReplyToId: String? = null
|
|
|
|
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
|
|
|
|
2020-04-18 15:06:24 +02:00
|
|
|
private var contentWarningStateChanged: Boolean = false
|
2020-10-02 18:32:46 +02:00
|
|
|
private var modifiedInitialState: Boolean = false
|
2022-07-27 21:06:51 +02:00
|
|
|
private var hasScheduledTimeChanged: Boolean = false
|
2020-04-18 15:06:24 +02:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
|
|
|
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
2022-04-21 18:46:21 +02:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
|
|
|
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
|
|
|
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
|
|
|
|
|
|
|
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
|
|
|
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
|
|
|
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
|
|
|
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-05-03 19:12:35 +02:00
|
|
|
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
2022-07-26 20:24:50 +02:00
|
|
|
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-05-03 19:12:35 +02:00
|
|
|
private val mediaToJob = mutableMapOf<Int, Job>()
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-05-22 21:01:14 +02:00
|
|
|
// Used in ComposeActivity to pass state to result function when cropImage contract inflight
|
|
|
|
var cropImageItemOld: QueuedMedia? = null
|
|
|
|
|
2022-08-03 17:23:54 +02:00
|
|
|
private var setupComplete = false
|
|
|
|
|
2022-04-21 18:46:21 +02:00
|
|
|
suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
|
|
|
try {
|
2022-07-26 20:24:50 +02:00
|
|
|
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
|
2022-05-03 19:12:35 +02:00
|
|
|
val mediaItems = media.value
|
2022-04-21 18:46:21 +02:00
|
|
|
if (type != QueuedMedia.Type.IMAGE &&
|
|
|
|
mediaItems.isNotEmpty() &&
|
|
|
|
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
|
|
|
) {
|
|
|
|
Result.failure(VideoOrImageException())
|
|
|
|
} else {
|
|
|
|
val queuedMedia = addMediaToQueue(type, uri, size, description)
|
|
|
|
Result.success(queuedMedia)
|
2021-06-28 22:04:34 +02:00
|
|
|
}
|
2022-04-21 18:46:21 +02:00
|
|
|
} catch (e: Exception) {
|
|
|
|
Result.failure(e)
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-05-22 21:01:14 +02:00
|
|
|
suspend fun addMediaToQueue(
|
2021-06-28 22:04:34 +02:00
|
|
|
type: QueuedMedia.Type,
|
|
|
|
uri: Uri,
|
|
|
|
mediaSize: Long,
|
2022-05-22 21:01:14 +02:00
|
|
|
description: String? = null,
|
|
|
|
replaceItem: QueuedMedia? = null
|
2021-01-21 18:57:09 +01:00
|
|
|
): QueuedMedia {
|
2022-05-22 21:01:14 +02:00
|
|
|
var stashMediaItem: QueuedMedia? = null
|
|
|
|
|
|
|
|
media.updateAndGet { mediaValue ->
|
2022-05-03 19:12:35 +02:00
|
|
|
val mediaItem = QueuedMedia(
|
|
|
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
|
|
|
uri = uri,
|
|
|
|
type = type,
|
|
|
|
mediaSize = mediaSize,
|
|
|
|
description = description
|
|
|
|
)
|
2022-05-22 21:01:14 +02:00
|
|
|
stashMediaItem = mediaItem
|
|
|
|
|
|
|
|
if (replaceItem != null) {
|
|
|
|
mediaToJob[replaceItem.localId]?.cancel()
|
|
|
|
mediaValue.map {
|
|
|
|
if (it.localId == replaceItem.localId) mediaItem else it
|
|
|
|
}
|
|
|
|
} else { // Append
|
|
|
|
mediaValue + mediaItem
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that
|
|
|
|
|
2022-04-21 18:46:21 +02:00
|
|
|
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
|
|
|
mediaUploader
|
2022-07-26 20:24:50 +02:00
|
|
|
.uploadMedia(mediaItem, instanceInfo.first())
|
2022-04-21 18:46:21 +02:00
|
|
|
.catch { error ->
|
2022-05-03 19:12:35 +02:00
|
|
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
|
2022-07-26 20:24:50 +02:00
|
|
|
uploadError.emit(error)
|
2022-04-21 18:46:21 +02:00
|
|
|
}
|
|
|
|
.collect { event ->
|
2022-05-03 19:12:35 +02:00
|
|
|
val item = media.value.find { it.localId == mediaItem.localId }
|
2022-04-21 18:46:21 +02:00
|
|
|
?: return@collect
|
2019-12-19 19:09:40 +01:00
|
|
|
val newMediaItem = when (event) {
|
|
|
|
is UploadEvent.ProgressEvent ->
|
|
|
|
item.copy(uploadPercent = event.percentage)
|
|
|
|
is UploadEvent.FinishedEvent ->
|
2022-02-25 18:57:18 +01:00
|
|
|
item.copy(id = event.mediaId, uploadPercent = -1)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-05-03 19:12:35 +02:00
|
|
|
media.update { mediaValue ->
|
|
|
|
mediaValue.map { mediaItem ->
|
|
|
|
if (mediaItem.localId == newMediaItem.localId) {
|
|
|
|
newMediaItem
|
2021-06-28 22:04:34 +02:00
|
|
|
} else {
|
2022-05-03 19:12:35 +02:00
|
|
|
mediaItem
|
2021-06-28 22:04:34 +02:00
|
|
|
}
|
2022-05-03 19:12:35 +02:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2021-06-28 22:04:34 +02:00
|
|
|
}
|
2022-04-21 18:46:21 +02:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
return mediaItem
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
2022-05-03 19:12:35 +02:00
|
|
|
media.update { mediaValue ->
|
|
|
|
val mediaItem = QueuedMedia(
|
|
|
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
|
|
|
uri = uri,
|
|
|
|
type = type,
|
|
|
|
mediaSize = 0,
|
|
|
|
uploadPercent = -1,
|
|
|
|
id = id,
|
|
|
|
description = description
|
|
|
|
)
|
|
|
|
mediaValue + mediaItem
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
2022-04-21 18:46:21 +02:00
|
|
|
mediaToJob[item.localId]?.cancel()
|
2022-05-12 18:21:43 +02:00
|
|
|
media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } }
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
fun toggleMarkSensitive() {
|
|
|
|
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
fun didChange(content: String?, contentWarning: String?): Boolean {
|
|
|
|
|
2021-06-28 22:04:34 +02:00
|
|
|
val textChanged = !(
|
|
|
|
content.isNullOrEmpty() ||
|
|
|
|
startingText?.startsWith(content.toString()) ?: false
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
val contentWarningChanged = showContentWarning.value &&
|
2021-06-28 22:04:34 +02:00
|
|
|
!contentWarning.isNullOrEmpty() &&
|
|
|
|
!startingContentWarning.startsWith(contentWarning.toString())
|
2022-06-30 20:51:05 +02:00
|
|
|
val mediaChanged = media.value.isNotEmpty()
|
2019-12-19 19:09:40 +01:00
|
|
|
val pollChanged = poll.value != null
|
2022-07-27 21:06:51 +02:00
|
|
|
val didScheduledTimeChange = hasScheduledTimeChanged
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-07-27 21:06:51 +02:00
|
|
|
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2020-04-18 15:06:24 +02:00
|
|
|
fun contentWarningChanged(value: Boolean) {
|
|
|
|
showContentWarning.value = value
|
|
|
|
contentWarningStateChanged = true
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
fun deleteDraft() {
|
2021-06-24 21:23:29 +02:00
|
|
|
viewModelScope.launch {
|
|
|
|
if (draftId != 0) {
|
|
|
|
draftHelper.deleteDraftAndAttachments(draftId)
|
|
|
|
}
|
2021-01-21 18:57:09 +01:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-05-09 19:39:43 +02:00
|
|
|
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 ->
|
|
|
|
mediaValue.uri.scheme == "https"
|
|
|
|
}
|
|
|
|
}
|
2021-01-21 18:57:09 +01:00
|
|
|
|
2022-05-09 19:39:43 +02:00
|
|
|
suspend fun saveDraft(content: String, contentWarning: String) {
|
|
|
|
val mediaUris: MutableList<String> = mutableListOf()
|
|
|
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
|
|
|
media.value.forEach { item ->
|
|
|
|
mediaUris.add(item.uri.toString())
|
|
|
|
mediaDescriptions.add(item.description)
|
2021-06-24 21:23:29 +02:00
|
|
|
}
|
2022-05-09 19:39:43 +02:00
|
|
|
|
|
|
|
draftHelper.saveDraft(
|
|
|
|
draftId = draftId,
|
|
|
|
accountId = accountManager.activeAccount?.id!!,
|
|
|
|
inReplyToId = inReplyToId,
|
|
|
|
content = content,
|
|
|
|
contentWarning = contentWarning,
|
2022-07-26 20:24:50 +02:00
|
|
|
sensitive = markMediaAsSensitive.value,
|
|
|
|
visibility = statusVisibility.value,
|
2022-05-09 19:39:43 +02:00
|
|
|
mediaUris = mediaUris,
|
|
|
|
mediaDescriptions = mediaDescriptions,
|
|
|
|
poll = poll.value,
|
2022-07-27 21:06:51 +02:00
|
|
|
failedToSend = false,
|
|
|
|
scheduledAt = scheduledAt.value
|
2022-05-09 19:39:43 +02:00
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send status to the server.
|
|
|
|
* Uses current state plus provided arguments.
|
|
|
|
*/
|
2022-07-26 20:24:50 +02:00
|
|
|
suspend fun sendStatus(
|
2021-06-28 22:04:34 +02:00
|
|
|
content: String,
|
|
|
|
spoilerText: String
|
2022-07-26 20:24:50 +02:00
|
|
|
) {
|
2020-10-25 18:41:11 +01:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
if (!scheduledTootId.isNullOrEmpty()) {
|
|
|
|
api.deleteScheduledStatus(scheduledTootId!!)
|
|
|
|
}
|
2020-10-25 18:41:11 +01:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
media
|
2021-06-28 22:04:34 +02:00
|
|
|
.filter { items -> items.all { it.uploadPercent == -1 } }
|
2022-07-26 20:24:50 +02:00
|
|
|
.first {
|
2022-04-28 20:37:31 +02:00
|
|
|
val mediaIds: MutableList<String> = mutableListOf()
|
|
|
|
val mediaUris: MutableList<Uri> = mutableListOf()
|
|
|
|
val mediaDescriptions: MutableList<String> = mutableListOf()
|
|
|
|
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
2022-07-26 20:24:50 +02:00
|
|
|
media.value.forEach { item ->
|
2021-06-28 22:04:34 +02:00
|
|
|
mediaIds.add(item.id!!)
|
|
|
|
mediaUris.add(item.uri)
|
|
|
|
mediaDescriptions.add(item.description ?: "")
|
2022-04-28 20:37:31 +02:00
|
|
|
mediaProcessed.add(false)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-03-20 20:21:42 +01:00
|
|
|
val tootToSend = StatusToSend(
|
2021-06-28 22:04:34 +02:00
|
|
|
text = content,
|
|
|
|
warningText = spoilerText,
|
2022-07-26 20:24:50 +02:00
|
|
|
visibility = statusVisibility.value.serverString(),
|
|
|
|
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
|
2021-06-28 22:04:34 +02:00
|
|
|
mediaIds = mediaIds,
|
|
|
|
mediaUris = mediaUris.map { it.toString() },
|
|
|
|
mediaDescriptions = mediaDescriptions,
|
|
|
|
scheduledAt = scheduledAt.value,
|
|
|
|
inReplyToId = inReplyToId,
|
|
|
|
poll = poll.value,
|
|
|
|
replyingStatusContent = null,
|
|
|
|
replyingStatusAuthorUsername = null,
|
|
|
|
accountId = accountManager.activeAccount!!.id,
|
|
|
|
draftId = draftId,
|
|
|
|
idempotencyKey = randomAlphanumericString(16),
|
2022-04-28 20:37:31 +02:00
|
|
|
retries = 0,
|
|
|
|
mediaProcessed = mediaProcessed
|
2021-06-28 22:04:34 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
serviceClient.sendToot(tootToSend)
|
2022-07-26 20:24:50 +02:00
|
|
|
true
|
2021-06-28 22:04:34 +02:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-05-03 19:12:35 +02:00
|
|
|
suspend fun updateDescription(localId: Int, description: String): Boolean {
|
|
|
|
val newMediaList = media.updateAndGet { mediaValue ->
|
|
|
|
mediaValue.map { mediaItem ->
|
|
|
|
if (mediaItem.localId == localId) {
|
|
|
|
mediaItem.copy(description = description)
|
|
|
|
} else {
|
|
|
|
mediaItem
|
|
|
|
}
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-05-03 19:12:35 +02:00
|
|
|
|
|
|
|
val updatedItem = newMediaList.find { it.localId == localId }
|
2022-04-21 18:46:21 +02:00
|
|
|
if (updatedItem?.id != null) {
|
|
|
|
return api.updateMedia(updatedItem.id, description)
|
|
|
|
.fold({
|
|
|
|
true
|
|
|
|
}, { throwable ->
|
|
|
|
Log.w(TAG, "failed to update media", throwable)
|
|
|
|
false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return true
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-05-17 19:55:37 +02:00
|
|
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
2019-12-19 19:09:40 +01:00
|
|
|
when (token[0]) {
|
|
|
|
'@' -> {
|
2022-05-30 20:03:40 +02:00
|
|
|
return api.searchAccountsSync(query = token.substring(1), limit = 10)
|
2022-05-17 19:55:37 +02:00
|
|
|
.fold({ accounts ->
|
|
|
|
accounts.map { AutocompleteResult.AccountResult(it) }
|
|
|
|
}, { e ->
|
|
|
|
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
|
|
|
emptyList()
|
|
|
|
})
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
'#' -> {
|
2022-05-30 20:03:40 +02:00
|
|
|
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
2022-05-17 19:55:37 +02:00
|
|
|
.fold({ searchResult ->
|
|
|
|
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
|
|
|
}, { e ->
|
|
|
|
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
|
|
|
emptyList()
|
|
|
|
})
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
':' -> {
|
2022-07-26 20:24:50 +02:00
|
|
|
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
|
2022-05-17 19:55:37 +02:00
|
|
|
val incomplete = token.substring(1)
|
|
|
|
|
|
|
|
return emojiList.filter { emoji ->
|
|
|
|
emoji.shortcode.contains(incomplete, ignoreCase = true)
|
|
|
|
}.sortedBy { emoji ->
|
|
|
|
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
|
|
|
|
}.map { emoji ->
|
|
|
|
AutocompleteResult.EmojiResult(emoji)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
Log.w(TAG, "Unexpected autocompletion token: $token")
|
|
|
|
return emptyList()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
|
|
|
|
2022-08-03 17:23:54 +02:00
|
|
|
if (setupComplete) {
|
2021-01-21 18:57:09 +01:00
|
|
|
return
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
|
|
|
|
|
|
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
|
|
|
startingVisibility = Status.Visibility.byNum(
|
2021-06-28 22:04:34 +02:00
|
|
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
inReplyToId = composeOptions?.inReplyToId
|
2021-01-21 18:57:09 +01:00
|
|
|
|
2020-10-02 18:32:46 +02:00
|
|
|
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
val contentWarning = composeOptions?.contentWarning
|
|
|
|
if (contentWarning != null) {
|
|
|
|
startingContentWarning = contentWarning
|
|
|
|
}
|
2020-04-18 15:06:24 +02:00
|
|
|
if (!contentWarningStateChanged) {
|
|
|
|
showContentWarning.value = !contentWarning.isNullOrBlank()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
// recreate media list
|
2021-01-21 18:57:09 +01:00
|
|
|
val draftAttachments = composeOptions?.draftAttachments
|
2021-05-16 19:17:56 +02:00
|
|
|
if (draftAttachments != null) {
|
2021-01-21 18:57:09 +01:00
|
|
|
// when coming from DraftActivity
|
2022-05-03 19:12:35 +02:00
|
|
|
viewModelScope.launch {
|
|
|
|
draftAttachments.forEach { attachment ->
|
2022-04-21 18:46:21 +02:00
|
|
|
pickMedia(attachment.uri, attachment.description)
|
|
|
|
}
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
2021-01-21 18:57:09 +01:00
|
|
|
// when coming from redraft or ScheduledTootActivity
|
2019-12-19 19:09:40 +01:00
|
|
|
val mediaType = when (a.type) {
|
|
|
|
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
|
|
|
|
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
2020-01-16 19:05:52 +01:00
|
|
|
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
draftId = composeOptions?.draftId ?: 0
|
|
|
|
scheduledTootId = composeOptions?.scheduledTootId
|
2022-03-20 20:21:42 +01:00
|
|
|
startingText = composeOptions?.content
|
2019-12-21 15:56:07 +01:00
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
|
|
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
|
|
|
startingVisibility = tootVisibility
|
|
|
|
}
|
2019-12-27 21:02:16 +01:00
|
|
|
statusVisibility.value = startingVisibility
|
2019-12-19 19:09:40 +01:00
|
|
|
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
|
2022-08-03 17:23:54 +02:00
|
|
|
|
|
|
|
setupComplete = true
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fun updatePoll(newPoll: NewPoll) {
|
|
|
|
poll.value = newPoll
|
|
|
|
}
|
|
|
|
|
|
|
|
fun updateScheduledAt(newScheduledAt: String?) {
|
2022-07-27 21:06:51 +02:00
|
|
|
if (newScheduledAt != scheduledAt.value) {
|
|
|
|
hasScheduledTimeChanged = true
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
scheduledAt.value = newScheduledAt
|
|
|
|
}
|
|
|
|
|
|
|
|
private companion object {
|
|
|
|
const val TAG = "ComposeViewModel"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
/**
|
|
|
|
* Thrown when trying to add an image when video is already present or the other way around
|
|
|
|
*/
|
2021-01-31 18:58:45 +01:00
|
|
|
class VideoOrImageException : Exception()
|