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 ba13556a5..303c6cbf3 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 @@ -1040,7 +1040,7 @@ class ComposeActivity : override fun onVisibilityChanged(visibility: Status.Visibility) { composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - viewModel.statusVisibility.value = visibility + viewModel.changeStatusVisibility(visibility) } @VisibleForTesting 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 639779eaa..82b2f3583 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 @@ -45,7 +45,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update @@ -86,16 +88,25 @@ class ComposeViewModel @Inject constructor( val emoji: SharedFlow> = instanceInfoRepo::getEmojis.asFlow() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val markMediaAsSensitive: MutableStateFlow = + private val _markMediaAsSensitive = MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + val markMediaAsSensitive: StateFlow = _markMediaAsSensitive.asStateFlow() - val statusVisibility: MutableStateFlow = - MutableStateFlow(Status.Visibility.UNKNOWN) - val showContentWarning: MutableStateFlow = MutableStateFlow(false) - val poll: MutableStateFlow = MutableStateFlow(null) - val scheduledAt: MutableStateFlow = MutableStateFlow(null) + private val _statusVisibility = MutableStateFlow(Status.Visibility.UNKNOWN) + val statusVisibility: StateFlow = _statusVisibility.asStateFlow() + + private val _showContentWarning = MutableStateFlow(false) + val showContentWarning: StateFlow = _showContentWarning.asStateFlow() + + private val _poll = MutableStateFlow(null as NewPoll?) + val poll: StateFlow = _poll.asStateFlow() + + private val _scheduledAt = MutableStateFlow(null as String?) + val scheduledAt: StateFlow = _scheduledAt.asStateFlow() + + private val _media = MutableStateFlow(emptyList()) + val media: StateFlow> = _media.asStateFlow() - val media: MutableStateFlow> = MutableStateFlow(emptyList()) val uploadError = MutableSharedFlow( replay = 0, @@ -103,7 +114,8 @@ class ComposeViewModel @Inject constructor( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - val closeConfirmation = MutableStateFlow(ConfirmationKind.NONE) + private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE) + val closeConfirmation: StateFlow = _closeConfirmation.asStateFlow() private lateinit var composeKind: ComposeKind @@ -121,7 +133,7 @@ class ComposeViewModel @Inject constructor( ) { try { val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) - val mediaItems = media.value + val mediaItems = _media.value if (type != QueuedMedia.Type.IMAGE && mediaItems.isNotEmpty() && mediaItems[0].type == QueuedMedia.Type.IMAGE @@ -146,7 +158,7 @@ class ComposeViewModel @Inject constructor( ): QueuedMedia { var stashMediaItem: QueuedMedia? = null - media.update { mediaList -> + _media.update { mediaList -> val mediaItem = QueuedMedia( localId = mediaUploader.getNewLocalMediaId(), uri = uri, @@ -173,7 +185,7 @@ class ComposeViewModel @Inject constructor( mediaUploader .uploadMedia(mediaItem, instanceInfo.first()) .collect { event -> - val item = media.value.find { it.localId == mediaItem.localId } + val item = _media.value.find { it.localId == mediaItem.localId } ?: return@collect val newMediaItem = when (event) { is UploadEvent.ProgressEvent -> @@ -189,12 +201,12 @@ class ComposeViewModel @Inject constructor( } ) is UploadEvent.ErrorEvent -> { - media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } + _media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } uploadError.emit(event.error) return@collect } } - media.update { mediaList -> + _media.update { mediaList -> mediaList.map { mediaItem -> if (mediaItem.localId == newMediaItem.localId) { newMediaItem @@ -209,6 +221,10 @@ class ComposeViewModel @Inject constructor( return mediaItem } + fun changeStatusVisibility(visibility: Status.Visibility) { + _statusVisibility.value = visibility + } + private fun addUploadedMedia( id: String, type: QueuedMedia.Type, @@ -216,7 +232,7 @@ class ComposeViewModel @Inject constructor( description: String?, focus: Attachment.Focus? ) { - media.update { mediaList -> + _media.update { mediaList -> val mediaItem = QueuedMedia( localId = mediaUploader.getNewLocalMediaId(), uri = uri, @@ -234,12 +250,12 @@ class ComposeViewModel @Inject constructor( fun removeMediaFromQueue(item: QueuedMedia) { mediaUploader.cancelUploadScope(item.localId) - media.update { mediaList -> mediaList.filter { it.localId != item.localId } } + _media.update { mediaList -> mediaList.filter { it.localId != item.localId } } updateCloseConfirmation() } fun toggleMarkSensitive() { - this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true + this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true } fun updateContent(newContent: String?) { @@ -253,12 +269,12 @@ class ComposeViewModel @Inject constructor( } private fun updateCloseConfirmation() { - val contentWarning = if (showContentWarning.value) { + val contentWarning = if (_showContentWarning.value) { currentContentWarning } else { "" } - this.closeConfirmation.value = if (didChange(currentContent, contentWarning)) { + this._closeConfirmation.value = if (didChange(currentContent, contentWarning)) { when (composeKind) { ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) { ConfirmationKind.NONE @@ -281,19 +297,19 @@ class ComposeViewModel @Inject constructor( private fun didChange(content: String?, contentWarning: String?): Boolean { val textChanged = content.orEmpty() != startingText.orEmpty() val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning - val mediaChanged = media.value.isNotEmpty() - val pollChanged = poll.value != null + val mediaChanged = _media.value.isNotEmpty() + val pollChanged = _poll.value != null val didScheduledTimeChange = hasScheduledTimeChanged return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange } private fun isEmpty(content: String?, contentWarning: String?): Boolean { - return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && media.value.isEmpty() && poll.value == null) + return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && _media.value.isEmpty() && _poll.value == null) } fun contentWarningChanged(value: Boolean) { - showContentWarning.value = value + _showContentWarning.value = value contentWarningStateChanged = true updateCloseConfirmation() } @@ -307,12 +323,12 @@ class ComposeViewModel @Inject constructor( } fun stopUploads() { - mediaUploader.cancelUploadScope(*media.value.map { it.localId }.toIntArray()) + 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 -> + return _media.value.any { mediaValue -> mediaValue.uri.scheme == "https" } } @@ -321,7 +337,7 @@ class ComposeViewModel @Inject constructor( val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() val mediaFocus: MutableList = mutableListOf() - for (item in media.value) { + for (item in _media.value) { mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) mediaFocus.add(item.focus) @@ -333,15 +349,15 @@ class ComposeViewModel @Inject constructor( inReplyToId = inReplyToId, content = content, contentWarning = contentWarning, - sensitive = markMediaAsSensitive.value, - visibility = statusVisibility.value, + sensitive = _markMediaAsSensitive.value, + visibility = _statusVisibility.value, mediaUris = mediaUris, mediaDescriptions = mediaDescriptions, mediaFocus = mediaFocus, - poll = poll.value, + poll = _poll.value, failedToSend = false, failedToSendAlert = false, - scheduledAt = scheduledAt.value, + scheduledAt = _scheduledAt.value, language = postLanguage, statusId = originalStatusId ) @@ -356,7 +372,7 @@ class ComposeViewModel @Inject constructor( api.deleteScheduledStatus(scheduledTootId!!) } - val attachedMedia = media.value.map { item -> + val attachedMedia = _media.value.map { item -> MediaToSend( localId = item.localId, id = item.id, @@ -369,12 +385,12 @@ class ComposeViewModel @Inject constructor( val tootToSend = StatusToSend( text = content, warningText = spoilerText, - visibility = statusVisibility.value.serverString(), - sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), + visibility = _statusVisibility.value.serverString(), + sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value), media = attachedMedia, - scheduledAt = scheduledAt.value, + scheduledAt = _scheduledAt.value, inReplyToId = inReplyToId, - poll = poll.value, + poll = _poll.value, replyingStatusContent = null, replyingStatusAuthorUsername = null, accountId = accountId, @@ -389,7 +405,7 @@ class ComposeViewModel @Inject constructor( } private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) { - media.update { mediaList -> + _media.update { mediaList -> mediaList.map { mediaItem -> if (mediaItem.localId == localId) { mutator(mediaItem) @@ -478,7 +494,7 @@ class ComposeViewModel @Inject constructor( startingContentWarning = contentWarning } if (!contentWarningStateChanged) { - showContentWarning.value = !contentWarning.isNullOrBlank() + _showContentWarning.value = !contentWarning.isNullOrBlank() } // recreate media list @@ -512,7 +528,7 @@ class ComposeViewModel @Inject constructor( if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { startingVisibility = tootVisibility } - statusVisibility.value = startingVisibility + _statusVisibility.value = startingVisibility val mentionedUsernames = composeOptions?.mentionedUsernames if (mentionedUsernames != null) { val builder = StringBuilder() @@ -524,13 +540,13 @@ class ComposeViewModel @Inject constructor( startingText = builder.toString() } - scheduledAt.value = composeOptions?.scheduledAt + _scheduledAt.value = composeOptions?.scheduledAt - composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } + composeOptions?.sensitive?.let { _markMediaAsSensitive.value = it } val poll = composeOptions?.poll if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) { - this.poll.value = poll + this._poll.value = poll } replyingStatusContent = composeOptions?.replyingStatusContent replyingStatusAuthor = composeOptions?.replyingStatusAuthor @@ -541,16 +557,16 @@ class ComposeViewModel @Inject constructor( } fun updatePoll(newPoll: NewPoll?) { - poll.value = newPoll + _poll.value = newPoll updateCloseConfirmation() } fun updateScheduledAt(newScheduledAt: String?) { - if (newScheduledAt != scheduledAt.value) { + if (newScheduledAt != _scheduledAt.value) { hasScheduledTimeChanged = true } - scheduledAt.value = newScheduledAt + _scheduledAt.value = newScheduledAt } val editing: Boolean diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt index f98d983af..83c8dacf1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt @@ -11,79 +11,91 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.isHttpNotFound import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() { private var originalFilter: Filter? = null - val title = MutableStateFlow("") - val keywords = MutableStateFlow(listOf()) - val action = MutableStateFlow(Filter.Action.WARN) - val duration = MutableStateFlow(0) - val contexts = MutableStateFlow(listOf()) + + private val _title = MutableStateFlow("") + val title: StateFlow = _title.asStateFlow() + + private val _keywords = MutableStateFlow(listOf()) + val keywords: StateFlow> = _keywords.asStateFlow() + + private val _action = MutableStateFlow(Filter.Action.WARN) + val action: StateFlow = _action.asStateFlow() + + private val _duration = MutableStateFlow(0) + val duration: StateFlow = _duration.asStateFlow() + + private val _contexts = MutableStateFlow(listOf()) + val contexts: StateFlow> = _contexts.asStateFlow() fun load(filter: Filter) { originalFilter = filter - title.value = filter.title - keywords.value = filter.keywords - action.value = filter.action - duration.value = if (filter.expiresAt == null) { + _title.value = filter.title + _keywords.value = filter.keywords + _action.value = filter.action + _duration.value = if (filter.expiresAt == null) { 0 } else { -1 } - contexts.value = filter.kinds + _contexts.value = filter.kinds } fun addKeyword(keyword: FilterKeyword) { - keywords.value += keyword + _keywords.value += keyword } fun deleteKeyword(keyword: FilterKeyword) { - keywords.value = keywords.value.filterNot { it == keyword } + _keywords.value = _keywords.value.filterNot { it == keyword } } fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) { - val index = keywords.value.indexOf(original) + val index = _keywords.value.indexOf(original) if (index >= 0) { - keywords.value = keywords.value.toMutableList().apply { + _keywords.value = _keywords.value.toMutableList().apply { set(index, updated) } } } fun setTitle(title: String) { - this.title.value = title + this._title.value = title } fun setDuration(index: Int) { - duration.value = index + _duration.value = index } fun setAction(action: Filter.Action) { - this.action.value = action + this._action.value = action } fun addContext(context: Filter.Kind) { - if (!contexts.value.contains(context)) { - contexts.value += context + if (!_contexts.value.contains(context)) { + _contexts.value += context } } fun removeContext(context: Filter.Kind) { - contexts.value = contexts.value.filter { it != context } + _contexts.value = _contexts.value.filter { it != context } } fun validate(): Boolean { - return title.value.isNotBlank() && - keywords.value.isNotEmpty() && - contexts.value.isNotEmpty() + return _title.value.isNotBlank() && + _keywords.value.isNotEmpty() && + _contexts.value.isNotEmpty() } suspend fun saveChanges(context: Context): Boolean { - val contexts = contexts.value.map { it.kind } - val title = title.value - val durationIndex = duration.value - val action = action.value.action + val contexts = _contexts.value.map { it.kind } + val title = _title.value + val durationIndex = _duration.value + val action = _action.value.action return withContext(viewModelScope.coroutineContext) { originalFilter?.let { filter -> @@ -108,7 +120,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub ).fold( { newFilter -> // This is _terrible_, but the all-in-one update filter api Just Doesn't Work - return keywords.value.map { keyword -> + return _keywords.value.map { keyword -> api.addFilterKeyword( filterId = newFilter.id, keyword = keyword.keyword, @@ -144,7 +156,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub ).fold( { // This is _terrible_, but the all-in-one update filter api Just Doesn't Work - val results = keywords.value.map { keyword -> + val results = _keywords.value.map { keyword -> if (keyword.id.isEmpty()) { api.addFilterKeyword(filterId = originalFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord) } else { @@ -152,7 +164,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub } } + originalFilter.keywords.filter { keyword -> // Deleted keywords - keywords.value.none { it.id == keyword.id } + _keywords.value.none { it.id == keyword.id } }.map { api.deleteFilterKeyword(it.id) } return results.none { it.isFailure } @@ -170,13 +182,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub } private suspend fun createFilterV1(context: List, expiresInSeconds: Int?): Boolean { - return keywords.value.map { keyword -> + return _keywords.value.map { keyword -> api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds) }.none { it.isFailure } } private suspend fun updateFilterV1(context: List, expiresInSeconds: Int?): Boolean { - val results = keywords.value.map { keyword -> + val results = _keywords.value.map { keyword -> if (originalFilter == null) { api.createFilterV1( phrase = keyword.keyword, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt index 01aa34288..6083fad07 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt @@ -23,13 +23,15 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.isHttpNotFound import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class LoginWebViewViewModel @Inject constructor( private val api: MastodonApi ) : ViewModel() { - val instanceRules: MutableStateFlow> = MutableStateFlow(emptyList()) + private val _instanceRules = MutableStateFlow(emptyList()) + val instanceRules = _instanceRules.asStateFlow() private var domain: String? = null @@ -39,13 +41,13 @@ class LoginWebViewViewModel @Inject constructor( viewModelScope.launch { api.getInstance().fold( { instance -> - instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text } + _instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text } }, { throwable -> if (throwable.isHttpNotFound()) { api.getInstanceV1(domain).fold( { instance -> - instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty() + _instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty() }, { throwable -> Log.w( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt index b864b3720..5ed9b08b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -51,6 +51,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -64,9 +65,8 @@ class ViewThreadViewModel @Inject constructor( private val gson: Gson ) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(ThreadUiState.Loading) - val uiState: Flow - get() = _uiState + private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState) + val uiState: Flow = _uiState.asStateFlow() private val _errors = MutableSharedFlow( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt index 5f5258c83..a110c57a9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -45,7 +45,7 @@ import org.pageseeder.xmlwriter.XMLStringWriter class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(EditsUiState.Initial) + private val _uiState = MutableStateFlow(EditsUiState.Initial as EditsUiState) val uiState: StateFlow = _uiState.asStateFlow() /** The API call to fetch edit history returned less than two items */ diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index 1a1b7957a..d3ac80baa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -73,7 +74,8 @@ class EditProfileViewModel @Inject constructor( val instanceData: Flow = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val isChanged = MutableStateFlow(false) + private val _isChanged = MutableStateFlow(false) + val isChanged = _isChanged.asStateFlow() private var apiProfileAccount: Account? = null @@ -106,7 +108,7 @@ class EditProfileViewModel @Inject constructor( } internal fun dataChanged(newProfileData: ProfileDataInUi) { - isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges() + _isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges() } internal fun save(newProfileData: ProfileDataInUi) {