Don't use mutable state flows in UI (#4336)

This commit is contained in:
Zongle Wang 2024-03-27 18:17:42 +08:00 committed by GitHub
parent 3274bd2660
commit a3d87de8ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 117 additions and 85 deletions

View File

@ -1040,7 +1040,7 @@ class ComposeActivity :
override fun onVisibilityChanged(visibility: Status.Visibility) { override fun onVisibilityChanged(visibility: Status.Visibility) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.statusVisibility.value = visibility viewModel.changeStatusVisibility(visibility)
} }
@VisibleForTesting @VisibleForTesting

View File

@ -45,7 +45,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -86,16 +88,25 @@ class ComposeViewModel @Inject constructor(
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow() val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val markMediaAsSensitive: MutableStateFlow<Boolean> = private val _markMediaAsSensitive =
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
val markMediaAsSensitive: StateFlow<Boolean> = _markMediaAsSensitive.asStateFlow()
val statusVisibility: MutableStateFlow<Status.Visibility> = private val _statusVisibility = MutableStateFlow(Status.Visibility.UNKNOWN)
MutableStateFlow(Status.Visibility.UNKNOWN) val statusVisibility: StateFlow<Status.Visibility> = _statusVisibility.asStateFlow()
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null) private val _showContentWarning = MutableStateFlow(false)
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null) val showContentWarning: StateFlow<Boolean> = _showContentWarning.asStateFlow()
private val _poll = MutableStateFlow(null as NewPoll?)
val poll: StateFlow<NewPoll?> = _poll.asStateFlow()
private val _scheduledAt = MutableStateFlow(null as String?)
val scheduledAt: StateFlow<String?> = _scheduledAt.asStateFlow()
private val _media = MutableStateFlow(emptyList<QueuedMedia>())
val media: StateFlow<List<QueuedMedia>> = _media.asStateFlow()
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
val uploadError = val uploadError =
MutableSharedFlow<Throwable>( MutableSharedFlow<Throwable>(
replay = 0, replay = 0,
@ -103,7 +114,8 @@ class ComposeViewModel @Inject constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
val closeConfirmation = MutableStateFlow(ConfirmationKind.NONE) private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
val closeConfirmation: StateFlow<ConfirmationKind> = _closeConfirmation.asStateFlow()
private lateinit var composeKind: ComposeKind private lateinit var composeKind: ComposeKind
@ -121,7 +133,7 @@ class ComposeViewModel @Inject constructor(
) { ) {
try { try {
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
val mediaItems = media.value val mediaItems = _media.value
if (type != QueuedMedia.Type.IMAGE && if (type != QueuedMedia.Type.IMAGE &&
mediaItems.isNotEmpty() && mediaItems.isNotEmpty() &&
mediaItems[0].type == QueuedMedia.Type.IMAGE mediaItems[0].type == QueuedMedia.Type.IMAGE
@ -146,7 +158,7 @@ class ComposeViewModel @Inject constructor(
): QueuedMedia { ): QueuedMedia {
var stashMediaItem: QueuedMedia? = null var stashMediaItem: QueuedMedia? = null
media.update { mediaList -> _media.update { mediaList ->
val mediaItem = QueuedMedia( val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(), localId = mediaUploader.getNewLocalMediaId(),
uri = uri, uri = uri,
@ -173,7 +185,7 @@ class ComposeViewModel @Inject constructor(
mediaUploader mediaUploader
.uploadMedia(mediaItem, instanceInfo.first()) .uploadMedia(mediaItem, instanceInfo.first())
.collect { event -> .collect { event ->
val item = media.value.find { it.localId == mediaItem.localId } val item = _media.value.find { it.localId == mediaItem.localId }
?: return@collect ?: return@collect
val newMediaItem = when (event) { val newMediaItem = when (event) {
is UploadEvent.ProgressEvent -> is UploadEvent.ProgressEvent ->
@ -189,12 +201,12 @@ class ComposeViewModel @Inject constructor(
} }
) )
is UploadEvent.ErrorEvent -> { 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) uploadError.emit(event.error)
return@collect return@collect
} }
} }
media.update { mediaList -> _media.update { mediaList ->
mediaList.map { mediaItem -> mediaList.map { mediaItem ->
if (mediaItem.localId == newMediaItem.localId) { if (mediaItem.localId == newMediaItem.localId) {
newMediaItem newMediaItem
@ -209,6 +221,10 @@ class ComposeViewModel @Inject constructor(
return mediaItem return mediaItem
} }
fun changeStatusVisibility(visibility: Status.Visibility) {
_statusVisibility.value = visibility
}
private fun addUploadedMedia( private fun addUploadedMedia(
id: String, id: String,
type: QueuedMedia.Type, type: QueuedMedia.Type,
@ -216,7 +232,7 @@ class ComposeViewModel @Inject constructor(
description: String?, description: String?,
focus: Attachment.Focus? focus: Attachment.Focus?
) { ) {
media.update { mediaList -> _media.update { mediaList ->
val mediaItem = QueuedMedia( val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(), localId = mediaUploader.getNewLocalMediaId(),
uri = uri, uri = uri,
@ -234,12 +250,12 @@ class ComposeViewModel @Inject constructor(
fun removeMediaFromQueue(item: QueuedMedia) { fun removeMediaFromQueue(item: QueuedMedia) {
mediaUploader.cancelUploadScope(item.localId) mediaUploader.cancelUploadScope(item.localId)
media.update { mediaList -> mediaList.filter { it.localId != item.localId } } _media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
updateCloseConfirmation() updateCloseConfirmation()
} }
fun toggleMarkSensitive() { fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true
} }
fun updateContent(newContent: String?) { fun updateContent(newContent: String?) {
@ -253,12 +269,12 @@ class ComposeViewModel @Inject constructor(
} }
private fun updateCloseConfirmation() { private fun updateCloseConfirmation() {
val contentWarning = if (showContentWarning.value) { val contentWarning = if (_showContentWarning.value) {
currentContentWarning currentContentWarning
} else { } else {
"" ""
} }
this.closeConfirmation.value = if (didChange(currentContent, contentWarning)) { this._closeConfirmation.value = if (didChange(currentContent, contentWarning)) {
when (composeKind) { when (composeKind) {
ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) { ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) {
ConfirmationKind.NONE ConfirmationKind.NONE
@ -281,19 +297,19 @@ class ComposeViewModel @Inject constructor(
private fun didChange(content: String?, contentWarning: String?): Boolean { private fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = content.orEmpty() != startingText.orEmpty() val textChanged = content.orEmpty() != startingText.orEmpty()
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
val mediaChanged = media.value.isNotEmpty() val mediaChanged = _media.value.isNotEmpty()
val pollChanged = poll.value != null val pollChanged = _poll.value != null
val didScheduledTimeChange = hasScheduledTimeChanged val didScheduledTimeChange = hasScheduledTimeChanged
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
} }
private fun isEmpty(content: String?, contentWarning: String?): Boolean { 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) { fun contentWarningChanged(value: Boolean) {
showContentWarning.value = value _showContentWarning.value = value
contentWarningStateChanged = true contentWarningStateChanged = true
updateCloseConfirmation() updateCloseConfirmation()
} }
@ -307,12 +323,12 @@ class ComposeViewModel @Inject constructor(
} }
fun stopUploads() { fun stopUploads() {
mediaUploader.cancelUploadScope(*media.value.map { it.localId }.toIntArray()) mediaUploader.cancelUploadScope(*_media.value.map { it.localId }.toIntArray())
} }
fun shouldShowSaveDraftDialog(): Boolean { fun shouldShowSaveDraftDialog(): Boolean {
// if any of the media files need to be downloaded first it could take a while, so show a loading dialog // 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" mediaValue.uri.scheme == "https"
} }
} }
@ -321,7 +337,7 @@ class ComposeViewModel @Inject constructor(
val mediaUris: MutableList<String> = mutableListOf() val mediaUris: MutableList<String> = mutableListOf()
val mediaDescriptions: MutableList<String?> = mutableListOf() val mediaDescriptions: MutableList<String?> = mutableListOf()
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf() val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
for (item in media.value) { for (item in _media.value) {
mediaUris.add(item.uri.toString()) mediaUris.add(item.uri.toString())
mediaDescriptions.add(item.description) mediaDescriptions.add(item.description)
mediaFocus.add(item.focus) mediaFocus.add(item.focus)
@ -333,15 +349,15 @@ class ComposeViewModel @Inject constructor(
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
content = content, content = content,
contentWarning = contentWarning, contentWarning = contentWarning,
sensitive = markMediaAsSensitive.value, sensitive = _markMediaAsSensitive.value,
visibility = statusVisibility.value, visibility = _statusVisibility.value,
mediaUris = mediaUris, mediaUris = mediaUris,
mediaDescriptions = mediaDescriptions, mediaDescriptions = mediaDescriptions,
mediaFocus = mediaFocus, mediaFocus = mediaFocus,
poll = poll.value, poll = _poll.value,
failedToSend = false, failedToSend = false,
failedToSendAlert = false, failedToSendAlert = false,
scheduledAt = scheduledAt.value, scheduledAt = _scheduledAt.value,
language = postLanguage, language = postLanguage,
statusId = originalStatusId statusId = originalStatusId
) )
@ -356,7 +372,7 @@ class ComposeViewModel @Inject constructor(
api.deleteScheduledStatus(scheduledTootId!!) api.deleteScheduledStatus(scheduledTootId!!)
} }
val attachedMedia = media.value.map { item -> val attachedMedia = _media.value.map { item ->
MediaToSend( MediaToSend(
localId = item.localId, localId = item.localId,
id = item.id, id = item.id,
@ -369,12 +385,12 @@ class ComposeViewModel @Inject constructor(
val tootToSend = StatusToSend( val tootToSend = StatusToSend(
text = content, text = content,
warningText = spoilerText, warningText = spoilerText,
visibility = statusVisibility.value.serverString(), visibility = _statusVisibility.value.serverString(),
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
media = attachedMedia, media = attachedMedia,
scheduledAt = scheduledAt.value, scheduledAt = _scheduledAt.value,
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
poll = poll.value, poll = _poll.value,
replyingStatusContent = null, replyingStatusContent = null,
replyingStatusAuthorUsername = null, replyingStatusAuthorUsername = null,
accountId = accountId, accountId = accountId,
@ -389,7 +405,7 @@ class ComposeViewModel @Inject constructor(
} }
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) { private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
media.update { mediaList -> _media.update { mediaList ->
mediaList.map { mediaItem -> mediaList.map { mediaItem ->
if (mediaItem.localId == localId) { if (mediaItem.localId == localId) {
mutator(mediaItem) mutator(mediaItem)
@ -478,7 +494,7 @@ class ComposeViewModel @Inject constructor(
startingContentWarning = contentWarning startingContentWarning = contentWarning
} }
if (!contentWarningStateChanged) { if (!contentWarningStateChanged) {
showContentWarning.value = !contentWarning.isNullOrBlank() _showContentWarning.value = !contentWarning.isNullOrBlank()
} }
// recreate media list // recreate media list
@ -512,7 +528,7 @@ class ComposeViewModel @Inject constructor(
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
startingVisibility = tootVisibility startingVisibility = tootVisibility
} }
statusVisibility.value = startingVisibility _statusVisibility.value = startingVisibility
val mentionedUsernames = composeOptions?.mentionedUsernames val mentionedUsernames = composeOptions?.mentionedUsernames
if (mentionedUsernames != null) { if (mentionedUsernames != null) {
val builder = StringBuilder() val builder = StringBuilder()
@ -524,13 +540,13 @@ class ComposeViewModel @Inject constructor(
startingText = builder.toString() 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 val poll = composeOptions?.poll
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) { if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
this.poll.value = poll this._poll.value = poll
} }
replyingStatusContent = composeOptions?.replyingStatusContent replyingStatusContent = composeOptions?.replyingStatusContent
replyingStatusAuthor = composeOptions?.replyingStatusAuthor replyingStatusAuthor = composeOptions?.replyingStatusAuthor
@ -541,16 +557,16 @@ class ComposeViewModel @Inject constructor(
} }
fun updatePoll(newPoll: NewPoll?) { fun updatePoll(newPoll: NewPoll?) {
poll.value = newPoll _poll.value = newPoll
updateCloseConfirmation() updateCloseConfirmation()
} }
fun updateScheduledAt(newScheduledAt: String?) { fun updateScheduledAt(newScheduledAt: String?) {
if (newScheduledAt != scheduledAt.value) { if (newScheduledAt != _scheduledAt.value) {
hasScheduledTimeChanged = true hasScheduledTimeChanged = true
} }
scheduledAt.value = newScheduledAt _scheduledAt.value = newScheduledAt
} }
val editing: Boolean val editing: Boolean

View File

@ -11,79 +11,91 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() { class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
private var originalFilter: Filter? = null private var originalFilter: Filter? = null
val title = MutableStateFlow("")
val keywords = MutableStateFlow(listOf<FilterKeyword>()) private val _title = MutableStateFlow("")
val action = MutableStateFlow(Filter.Action.WARN) val title: StateFlow<String> = _title.asStateFlow()
val duration = MutableStateFlow(0)
val contexts = MutableStateFlow(listOf<Filter.Kind>()) private val _keywords = MutableStateFlow(listOf<FilterKeyword>())
val keywords: StateFlow<List<FilterKeyword>> = _keywords.asStateFlow()
private val _action = MutableStateFlow(Filter.Action.WARN)
val action: StateFlow<Filter.Action> = _action.asStateFlow()
private val _duration = MutableStateFlow(0)
val duration: StateFlow<Int> = _duration.asStateFlow()
private val _contexts = MutableStateFlow(listOf<Filter.Kind>())
val contexts: StateFlow<List<Filter.Kind>> = _contexts.asStateFlow()
fun load(filter: Filter) { fun load(filter: Filter) {
originalFilter = filter originalFilter = filter
title.value = filter.title _title.value = filter.title
keywords.value = filter.keywords _keywords.value = filter.keywords
action.value = filter.action _action.value = filter.action
duration.value = if (filter.expiresAt == null) { _duration.value = if (filter.expiresAt == null) {
0 0
} else { } else {
-1 -1
} }
contexts.value = filter.kinds _contexts.value = filter.kinds
} }
fun addKeyword(keyword: FilterKeyword) { fun addKeyword(keyword: FilterKeyword) {
keywords.value += keyword _keywords.value += keyword
} }
fun deleteKeyword(keyword: FilterKeyword) { fun deleteKeyword(keyword: FilterKeyword) {
keywords.value = keywords.value.filterNot { it == keyword } _keywords.value = _keywords.value.filterNot { it == keyword }
} }
fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) { fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) {
val index = keywords.value.indexOf(original) val index = _keywords.value.indexOf(original)
if (index >= 0) { if (index >= 0) {
keywords.value = keywords.value.toMutableList().apply { _keywords.value = _keywords.value.toMutableList().apply {
set(index, updated) set(index, updated)
} }
} }
} }
fun setTitle(title: String) { fun setTitle(title: String) {
this.title.value = title this._title.value = title
} }
fun setDuration(index: Int) { fun setDuration(index: Int) {
duration.value = index _duration.value = index
} }
fun setAction(action: Filter.Action) { fun setAction(action: Filter.Action) {
this.action.value = action this._action.value = action
} }
fun addContext(context: Filter.Kind) { fun addContext(context: Filter.Kind) {
if (!contexts.value.contains(context)) { if (!_contexts.value.contains(context)) {
contexts.value += context _contexts.value += context
} }
} }
fun removeContext(context: Filter.Kind) { fun removeContext(context: Filter.Kind) {
contexts.value = contexts.value.filter { it != context } _contexts.value = _contexts.value.filter { it != context }
} }
fun validate(): Boolean { fun validate(): Boolean {
return title.value.isNotBlank() && return _title.value.isNotBlank() &&
keywords.value.isNotEmpty() && _keywords.value.isNotEmpty() &&
contexts.value.isNotEmpty() _contexts.value.isNotEmpty()
} }
suspend fun saveChanges(context: Context): Boolean { suspend fun saveChanges(context: Context): Boolean {
val contexts = contexts.value.map { it.kind } val contexts = _contexts.value.map { it.kind }
val title = title.value val title = _title.value
val durationIndex = duration.value val durationIndex = _duration.value
val action = action.value.action val action = _action.value.action
return withContext(viewModelScope.coroutineContext) { return withContext(viewModelScope.coroutineContext) {
originalFilter?.let { filter -> originalFilter?.let { filter ->
@ -108,7 +120,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
).fold( ).fold(
{ newFilter -> { newFilter ->
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work // 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( api.addFilterKeyword(
filterId = newFilter.id, filterId = newFilter.id,
keyword = keyword.keyword, keyword = keyword.keyword,
@ -144,7 +156,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
).fold( ).fold(
{ {
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work // 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()) { if (keyword.id.isEmpty()) {
api.addFilterKeyword(filterId = originalFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord) api.addFilterKeyword(filterId = originalFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord)
} else { } else {
@ -152,7 +164,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
} }
} + originalFilter.keywords.filter { keyword -> } + originalFilter.keywords.filter { keyword ->
// Deleted keywords // Deleted keywords
keywords.value.none { it.id == keyword.id } _keywords.value.none { it.id == keyword.id }
}.map { api.deleteFilterKeyword(it.id) } }.map { api.deleteFilterKeyword(it.id) }
return results.none { it.isFailure } return results.none { it.isFailure }
@ -170,13 +182,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
} }
private suspend fun createFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean { private suspend fun createFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
return keywords.value.map { keyword -> return _keywords.value.map { keyword ->
api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds) api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds)
}.none { it.isFailure } }.none { it.isFailure }
} }
private suspend fun updateFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean { private suspend fun updateFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean {
val results = keywords.value.map { keyword -> val results = _keywords.value.map { keyword ->
if (originalFilter == null) { if (originalFilter == null) {
api.createFilterV1( api.createFilterV1(
phrase = keyword.keyword, phrase = keyword.keyword,

View File

@ -23,13 +23,15 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LoginWebViewViewModel @Inject constructor( class LoginWebViewViewModel @Inject constructor(
private val api: MastodonApi private val api: MastodonApi
) : ViewModel() { ) : ViewModel() {
val instanceRules: MutableStateFlow<List<String>> = MutableStateFlow(emptyList()) private val _instanceRules = MutableStateFlow(emptyList<String>())
val instanceRules = _instanceRules.asStateFlow()
private var domain: String? = null private var domain: String? = null
@ -39,13 +41,13 @@ class LoginWebViewViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
api.getInstance().fold( api.getInstance().fold(
{ instance -> { instance ->
instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text } _instanceRules.value = instance.rules.orEmpty().map { rule -> rule.text }
}, },
{ throwable -> { throwable ->
if (throwable.isHttpNotFound()) { if (throwable.isHttpNotFound()) {
api.getInstanceV1(domain).fold( api.getInstanceV1(domain).fold(
{ instance -> { instance ->
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty() _instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
}, },
{ throwable -> { throwable ->
Log.w( Log.w(

View File

@ -51,6 +51,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,9 +65,8 @@ class ViewThreadViewModel @Inject constructor(
private val gson: Gson private val gson: Gson
) : ViewModel() { ) : ViewModel() {
private val _uiState: MutableStateFlow<ThreadUiState> = MutableStateFlow(ThreadUiState.Loading) private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState)
val uiState: Flow<ThreadUiState> val uiState: Flow<ThreadUiState> = _uiState.asStateFlow()
get() = _uiState
private val _errors = private val _errors =
MutableSharedFlow<Throwable>( MutableSharedFlow<Throwable>(

View File

@ -45,7 +45,7 @@ import org.pageseeder.xmlwriter.XMLStringWriter
class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
private val _uiState: MutableStateFlow<EditsUiState> = MutableStateFlow(EditsUiState.Initial) private val _uiState = MutableStateFlow(EditsUiState.Initial as EditsUiState)
val uiState: StateFlow<EditsUiState> = _uiState.asStateFlow() val uiState: StateFlow<EditsUiState> = _uiState.asStateFlow()
/** The API call to fetch edit history returned less than two items */ /** The API call to fetch edit history returned less than two items */

View File

@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -73,7 +74,8 @@ class EditProfileViewModel @Inject constructor(
val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow() val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val isChanged = MutableStateFlow(false) private val _isChanged = MutableStateFlow(false)
val isChanged = _isChanged.asStateFlow()
private var apiProfileAccount: Account? = null private var apiProfileAccount: Account? = null
@ -106,7 +108,7 @@ class EditProfileViewModel @Inject constructor(
} }
internal fun dataChanged(newProfileData: ProfileDataInUi) { internal fun dataChanged(newProfileData: ProfileDataInUi) {
isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges() _isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges()
} }
internal fun save(newProfileData: ProfileDataInUi) { internal fun save(newProfileData: ProfileDataInUi) {