Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ package com.nextcloud.talk.conversationlist.viewmodels

import android.content.Context
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
Expand All @@ -21,6 +25,8 @@ import com.nextcloud.talk.conversationlist.ui.ConversationListEntry
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.invitation.data.InvitationsModel
import com.nextcloud.talk.invitation.data.InvitationsRepository
import com.nextcloud.talk.jobs.ConversationActionWorker
import com.nextcloud.talk.jobs.ConversationActionWorker.ConversationAction
import com.nextcloud.talk.logger.Logger
import com.nextcloud.talk.messagesearch.MessageSearchHelper
import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults
Expand Down Expand Up @@ -62,6 +68,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
Expand Down Expand Up @@ -890,35 +898,35 @@ class ConversationsListViewModel @AssistedInject constructor(
}
}

@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsUnread(conversation: ConversationModel) {
val original = conversation.copy()
val optimistic = conversation.copy(unreadMessages = 1)
val apiVersion = ApiUtils.getChatApiVersion(
currentUser.capabilities?.spreedCapability!!,
intArrayOf(ApiUtils.API_V1)
)
val url = ApiUtils.getUrlForChatReadMarker(apiVersion, currentUser.baseUrl, conversation.token)
viewModelScope.launch {
val result = optimisticAction(
apply = {
conversationListUpdater.markPendingUnread(conversation.internalId)
applyLocally(optimistic)
revertTo(original) { conversationListUpdater.clearPendingUnread(conversation.internalId) }
},
request = { conversationsRepository.markConversationAsUnread(credentials, url) }
)
conversationListUpdater.markPendingUnread(conversation.internalId)
applyLocally(conversation.copy(unreadMessages = 1))

_readUnreadState.value = result.fold(
onSuccess = { ConversationReadUnreadUiState.Success },
onFailure = {
logger.e(TAG, "Failed to mark conversation as unread", it)
ConversationReadUnreadUiState.Error
}
)
handOver(conversation, ConversationAction.MARK_UNREAD, "mark a conversation as unread") {
_readUnreadState.value = ConversationReadUnreadUiState.Error
}
_readUnreadState.value = ConversationReadUnreadUiState.Success
}
}

/**
* Puts back only the field [action] owns, on top of whatever the entry holds by now. Each action is
* carried by its own work, so writing the whole captured conversation back would undo a different
* action that succeeded while this one was still in flight.
*/
@VisibleForTesting
internal suspend fun restore(original: ConversationModel, action: ConversationAction) {
val current = repository.getLocallyStoredConversation(currentUser, original.token) ?: original
applyLocally(
when (action) {
ConversationAction.MARK_UNREAD -> current.copy(unreadMessages = original.unreadMessages)
ConversationAction.FAVORITE -> current.copy(favorite = original.favorite)
ConversationAction.ARCHIVE -> current.copy(hasArchived = original.hasArchived)
}
)
}

private suspend fun applyLocally(conversation: ConversationModel) {
withContext(Dispatchers.IO) { repository.updateConversation(conversation) }
}
Expand All @@ -941,94 +949,72 @@ class ConversationsListViewModel @AssistedInject constructor(
_archiveState.value = ArchiveUiState.None
}

@Suppress("Detekt.TooGenericExceptionCaught")
fun toggleConversationArchive(conversation: ConversationModel) {
val original = conversation.copy()
val desiredArchived = !conversation.hasArchived
val optimistic = conversation.copy(hasArchived = desiredArchived)
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val url = ApiUtils.getUrlForArchive(apiVersion, currentUser.baseUrl, conversation.token)

viewModelScope.launch {
val result = optimisticAction(
apply = {
conversationListUpdater.markPendingArchived(conversation.internalId, desiredArchived)
applyLocally(optimistic)
revertTo(original) {
conversationListUpdater.clearPendingArchived(conversation.internalId, desiredArchived)
}
},
request = {
if (desiredArchived) {
conversationsRepository.archiveConversation(credentials, url)
} else {
conversationsRepository.unarchiveConversation(credentials, url)
}
}
)
conversationListUpdater.markPendingArchived(conversation.internalId, desiredArchived)
applyLocally(conversation.copy(hasArchived = desiredArchived))

_archiveState.value = result.fold(
onSuccess = { ArchiveUiState.Success(desiredArchived, conversation.displayName) },
onFailure = {
logger.e(TAG, "Failed to toggle conversation archive state", it)
ArchiveUiState.Error
}
)
handOver(conversation, ConversationAction.ARCHIVE, "archive a conversation", desiredArchived) {
_archiveState.value = ArchiveUiState.Error
}
_archiveState.value = ArchiveUiState.Success(desiredArchived, conversation.displayName)
}
}

@Suppress("Detekt.TooGenericExceptionCaught")
fun addConversationToFavorites(conversation: ConversationModel) {
val original = conversation.copy()
val optimistic = conversation.copy(favorite = true)
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val url = ApiUtils.getUrlForRoomFavorite(apiVersion, currentUser.baseUrl, conversation.token)
setFavorite(conversation, favorite = true)
}

fun removeConversationFromFavorites(conversation: ConversationModel) {
setFavorite(conversation, favorite = false)
}

private fun setFavorite(conversation: ConversationModel, favorite: Boolean) {
viewModelScope.launch {
val result = optimisticAction(
apply = {
conversationListUpdater.markPendingFavorite(conversation.internalId, favorite = true)
applyLocally(optimistic)
revertTo(original) {
conversationListUpdater.clearPendingFavorite(conversation.internalId, favorite = true)
}
},
request = { conversationsRepository.addConversationToFavorites(credentials, url) }
)
conversationListUpdater.markPendingFavorite(conversation.internalId, favorite)
applyLocally(conversation.copy(favorite = favorite))

_favoriteState.value = result.fold(
onSuccess = { FavoriteUiState.Success },
onFailure = {
logger.e(TAG, "Failed to add conversation to favorites", it)
FavoriteUiState.Error
}
)
handOver(conversation, ConversationAction.FAVORITE, "favorite a conversation", favorite) {
_favoriteState.value = FavoriteUiState.Error
}
_favoriteState.value = FavoriteUiState.Success
}
}

@Suppress("Detekt.TooGenericExceptionCaught")
fun removeConversationFromFavorites(conversation: ConversationModel) {
val original = conversation.copy()
val optimistic = conversation.copy(favorite = false)
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
val url = ApiUtils.getUrlForRoomFavorite(apiVersion, currentUser.baseUrl, conversation.token)
viewModelScope.launch {
val result = optimisticAction(
apply = {
conversationListUpdater.markPendingFavorite(conversation.internalId, favorite = false)
applyLocally(optimistic)
revertTo(original) {
conversationListUpdater.clearPendingFavorite(conversation.internalId, favorite = false)
}
},
request = { conversationsRepository.removeConversationFromFavorites(credentials, url) }
)
/**
* Hands the change to [ConversationActionWorker], which sends it even if this screen or the whole
* process is gone by then. While this screen is still around, a change the worker finally gave up
* on puts [conversation] back and reports itself through [onFailed]; when it is not, the worker
* has released the guard and the next room list sync brings the server state back instead.
*/
private fun handOver(
conversation: ConversationModel,
action: ConversationAction,
description: String,
enabled: Boolean = true,
onFailed: () -> Unit
) {
val context = NextcloudTalkApplication.sharedApplication!!.applicationContext
val workId = ConversationActionWorker.enqueue(
context = context,
userId = currentUser.id!!,
roomToken = conversation.token,
action = action,
enabled = enabled
)

_favoriteState.value = result.fold(
onSuccess = { FavoriteUiState.Success },
onFailure = {
logger.e(TAG, "Failed to remove conversation from favorites", it)
FavoriteUiState.Error
}
)
viewModelScope.launch {
val outcome = WorkManager.getInstance(context).getWorkInfoByIdFlow(workId)
.filterNotNull()
.first { it.state.isFinished }

if (outcome.state == WorkInfo.State.FAILED) {
logger.e(TAG, "Failed to $description")
restore(conversation, action)
onFailed()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand Down
Loading
Loading