Skip to content

Commit

Permalink
chore: extract send message to viemodel (#2824)
Browse files Browse the repository at this point in the history
  • Loading branch information
Garzas authored Mar 27, 2024
1 parent de629ad commit fec0ad9
Show file tree
Hide file tree
Showing 20 changed files with 1,078 additions and 775 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import com.wire.android.ui.home.conversations.banner.ConversationBanner
import com.wire.android.ui.home.conversations.banner.ConversationBannerViewModel
import com.wire.android.ui.home.conversations.call.ConversationCallViewModel
import com.wire.android.ui.home.conversations.call.ConversationCallViewState
import com.wire.android.ui.home.conversations.composer.MessageComposerViewModel
import com.wire.android.ui.home.conversations.delete.DeleteMessageDialog
import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs
import com.wire.android.ui.home.conversations.edit.EditMessageMenuItems
Expand All @@ -130,6 +131,7 @@ import com.wire.android.ui.home.conversations.model.ExpirationStatus
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration
import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMenuItems
import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel
import com.wire.android.ui.home.gallery.MediaGalleryActionType
import com.wire.android.ui.home.gallery.MediaGalleryNavBackArgs
import com.wire.android.ui.home.messagecomposer.MessageComposer
Expand Down Expand Up @@ -188,6 +190,7 @@ fun ConversationScreen(
conversationCallViewModel: ConversationCallViewModel = hiltViewModel(),
conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(),
messageComposerViewModel: MessageComposerViewModel = hiltViewModel(),
sendMessageViewModel: SendMessageViewModel = hiltViewModel(),
conversationMigrationViewModel: ConversationMigrationViewModel = hiltViewModel(),
messageDraftViewModel: MessageDraftViewModel = hiltViewModel(),
groupDetailsScreenResultRecipient: ResultRecipient<GroupConversationDetailsScreenDestination, GroupConversationDetailsNavBackArgs>,
Expand Down Expand Up @@ -335,7 +338,7 @@ fun ConversationScreen(
NavigationCommand(MessageDetailsScreenDestination(conversationInfoViewModel.conversationId, messageId, isSelfMessage))
)
},
onSendMessage = messageComposerViewModel::trySendMessage,
onSendMessage = sendMessageViewModel::trySendMessage,
onDeleteMessage = conversationMessagesViewModel::showDeleteMessageDialog,
onAssetItemClicked = conversationMessagesViewModel::downloadOrFetchAssetAndShowDialog,
onImageFullScreenMode = { message, isSelfMessage ->
Expand Down Expand Up @@ -389,14 +392,14 @@ fun ConversationScreen(
}
},
onBackButtonClick = { conversationScreenOnBackButtonClick(messageComposerViewModel, focusManager, navigator) },
composerMessages = messageComposerViewModel.infoMessage,
composerMessages = sendMessageViewModel.infoMessage,
conversationMessages = conversationMessagesViewModel.infoMessage,
conversationMessagesViewModel = conversationMessagesViewModel,
onSelfDeletingMessageRead = messageComposerViewModel::startSelfDeletion,
onNewSelfDeletingMessagesStatus = messageComposerViewModel::updateSelfDeletingMessages,
tempWritableImageUri = messageComposerViewModel.tempWritableImageUri,
tempWritableVideoUri = messageComposerViewModel.tempWritableVideoUri,
onFailedMessageRetryClicked = messageComposerViewModel::retrySendingMessage,
onFailedMessageRetryClicked = sendMessageViewModel::retrySendingMessage,
requestMentions = messageComposerViewModel::searchMembersToMention,
onClearMentionSearchResult = messageComposerViewModel::clearMentionSearchResult,
onPermissionPermanentlyDenied = {
Expand Down Expand Up @@ -470,8 +473,8 @@ fun ConversationScreen(
}
)
AssetTooLargeDialog(
dialogState = messageComposerViewModel.assetTooLargeDialogState,
hideDialog = messageComposerViewModel::hideAssetTooLargeError
dialogState = sendMessageViewModel.assetTooLargeDialogState,
hideDialog = sendMessageViewModel::hideAssetTooLargeError
)
VisitLinkDialog(
dialogState = messageComposerViewModel.visitLinkDialogState,
Expand All @@ -489,15 +492,15 @@ fun ConversationScreen(
)

SureAboutMessagingInDegradedConversationDialog(
dialogState = messageComposerViewModel.sureAboutMessagingDialogState,
sendAnyway = messageComposerViewModel::acceptSureAboutSendingMessage,
hideDialog = messageComposerViewModel::dismissSureAboutSendingMessage
dialogState = sendMessageViewModel.sureAboutMessagingDialogState,
sendAnyway = sendMessageViewModel::acceptSureAboutSendingMessage,
hideDialog = sendMessageViewModel::dismissSureAboutSendingMessage
)

(messageComposerViewModel.sureAboutMessagingDialogState as? SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold)?.let {
(sendMessageViewModel.sureAboutMessagingDialogState as? SureAboutMessagingDialogState.Visible.ConversationUnderLegalHold)?.let {
LegalHoldSubjectMessageDialog(
dialogDismissed = messageComposerViewModel::dismissSureAboutSendingMessage,
sendAnywayClicked = messageComposerViewModel::acceptSureAboutSendingMessage,
dialogDismissed = sendMessageViewModel::dismissSureAboutSendingMessage,
sendAnywayClicked = sendMessageViewModel::acceptSureAboutSendingMessage,
)
}

Expand Down Expand Up @@ -852,7 +855,7 @@ private fun ConversationScreenContent(
}

@Composable
fun SnackBarMessage(
private fun SnackBarMessage(
composerMessages: SharedFlow<SnackBarMessage>,
conversationMessages: SharedFlow<SnackBarMessage>
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* 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.
*
* This program 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 this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.conversations.composer

import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.wire.android.mapper.ContactMapper
import com.wire.android.navigation.SavedStateViewModel
import com.wire.android.ui.home.conversations.ConversationNavArgs
import com.wire.android.ui.home.conversations.InvalidLinkDialogState
import com.wire.android.ui.home.conversations.MessageComposerViewState
import com.wire.android.ui.home.conversations.VisitLinkDialogState
import com.wire.android.ui.home.conversations.model.UIMessage
import com.wire.android.ui.navArgs
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.configuration.FileSharingStatus
import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.message.SelfDeletionTimer
import com.wire.kalium.logic.data.message.draft.MessageDraft
import com.wire.kalium.logic.data.user.OtherUser
import com.wire.kalium.logic.feature.conversation.InteractionAvailability
import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult
import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase
import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase
import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase
import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase
import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import javax.inject.Inject

@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class MessageComposerViewModel @Inject constructor(
override val savedStateHandle: SavedStateHandle,
private val dispatchers: DispatcherProvider,
private val isFileSharingEnabled: IsFileSharingEnabledUseCase,
private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase,
private val updateConversationReadDate: UpdateConversationReadDateUseCase,
private val contactMapper: ContactMapper,
private val membersToMention: MembersToMentionUseCase,
private val enqueueMessageSelfDeletion: EnqueueMessageSelfDeletionUseCase,
private val observeSelfDeletingMessages: ObserveSelfDeletionTimerSettingsForConversationUseCase,
private val persistNewSelfDeletingStatus: PersistNewSelfDeletionTimerUseCase,
private val sendTypingEvent: SendTypingEventUseCase,
private val saveMessageDraft: SaveMessageDraftUseCase
) : SavedStateViewModel(savedStateHandle) {

var messageComposerViewState = mutableStateOf(MessageComposerViewState())
private set

var tempWritableVideoUri: Uri? = null
private set

var tempWritableImageUri: Uri? = null
private set

private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs()
val conversationId: QualifiedID = conversationNavArgs.conversationId

var visitLinkDialogState: VisitLinkDialogState by mutableStateOf(
VisitLinkDialogState.Hidden
)

var invalidLinkDialogState: InvalidLinkDialogState by mutableStateOf(
InvalidLinkDialogState.Hidden
)

init {
observeIsTypingAvailable()
observeSelfDeletingMessagesStatus()
setFileSharingStatus()
}

private fun observeIsTypingAvailable() = viewModelScope.launch {
observeConversationInteractionAvailability(conversationId).collect { result ->
messageComposerViewState.value = messageComposerViewState.value.copy(
interactionAvailability = when (result) {
is IsInteractionAvailableResult.Failure -> InteractionAvailability.DISABLED
is IsInteractionAvailableResult.Success -> result.interactionAvailability
}
)
}
}

private fun observeSelfDeletingMessagesStatus() = viewModelScope.launch {
observeSelfDeletingMessages(
conversationId,
considerSelfUserSettings = true
).collect { selfDeletingStatus ->
messageComposerViewState.value =
messageComposerViewState.value.copy(selfDeletionTimer = selfDeletingStatus)
}
}

fun searchMembersToMention(searchQuery: String) {
viewModelScope.launch(dispatchers.io()) {
val members = membersToMention(conversationId, searchQuery).map {
contactMapper.fromOtherUser(it.user as OtherUser)
}

messageComposerViewState.value =
messageComposerViewState.value.copy(mentionSearchResult = members)
}
}

fun clearMentionSearchResult() {
messageComposerViewState.value =
messageComposerViewState.value.copy(mentionSearchResult = emptyList())
}

private fun setFileSharingStatus() {
// TODO: handle restriction when sending assets
viewModelScope.launch {
messageComposerViewState.value = when (isFileSharingEnabled().state) {
FileSharingStatus.Value.Disabled,
is FileSharingStatus.Value.EnabledSome ->
messageComposerViewState.value.copy(isFileSharingEnabled = false)

FileSharingStatus.Value.EnabledAll ->
messageComposerViewState.value.copy(isFileSharingEnabled = true)
}
}
}

fun updateConversationReadDate(utcISO: String) {
viewModelScope.launch(dispatchers.io()) {
updateConversationReadDate(conversationId, Instant.parse(utcISO))
}
}

fun startSelfDeletion(uiMessage: UIMessage) {
enqueueMessageSelfDeletion(conversationId, uiMessage.header.messageId)
}

fun updateSelfDeletingMessages(newSelfDeletionTimer: SelfDeletionTimer) =
viewModelScope.launch {
messageComposerViewState.value =
messageComposerViewState.value.copy(selfDeletionTimer = newSelfDeletionTimer)
persistNewSelfDeletingStatus(conversationId, newSelfDeletionTimer)
}

fun hideVisitLinkDialog() {
visitLinkDialogState = VisitLinkDialogState.Hidden
}

fun hideInvalidLinkError() {
invalidLinkDialogState = InvalidLinkDialogState.Hidden
}

fun sendTypingEvent(typingIndicatorMode: TypingIndicatorMode) {
viewModelScope.launch {
sendTypingEvent(conversationId, typingIndicatorMode)
}
}

fun saveDraft(messageDraft: MessageDraft) {
viewModelScope.launch {
saveMessageDraft(conversationId, messageDraft)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.R
import com.wire.android.media.audiomessage.AudioState
import com.wire.android.model.SnackBarMessage
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.style.PopUpNavigationAnimation
Expand All @@ -54,22 +56,22 @@ import com.wire.android.ui.common.calculateCurrentTab
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog
import com.wire.android.ui.common.scaffold.WireScaffold
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topBarElevation
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.MediaGalleryScreenDestination
import com.wire.android.ui.home.conversations.DownloadedAssetDialog
import com.wire.android.ui.home.conversations.MessageComposerViewModel
import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState
import com.wire.android.ui.home.conversations.SnackBarMessage
import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModel
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.data.id.ConversationId
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch

@RootNavGraph
Expand All @@ -81,8 +83,7 @@ import kotlinx.coroutines.launch
fun ConversationMediaScreen(
navigator: Navigator,
conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = hiltViewModel(),
conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(),
messageComposerViewModel: MessageComposerViewModel = hiltViewModel()
conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel()
) {
val permissionPermanentlyDeniedDialogState =
rememberVisibilityState<PermissionPermanentlyDeniedDialogState>()
Expand Down Expand Up @@ -129,10 +130,7 @@ fun ConversationMediaScreen(
hideDialog = permissionPermanentlyDeniedDialogState::dismiss
)

SnackBarMessage(
messageComposerViewModel.infoMessage,
conversationMessagesViewModel.infoMessage
)
SnackBarMessage(conversationMessagesViewModel.infoMessage)
}

@OptIn(ExperimentalFoundationApi::class)
Expand Down Expand Up @@ -207,6 +205,20 @@ private fun Content(
}
}

@Composable
private fun SnackBarMessage(infoMessages: SharedFlow<SnackBarMessage>) {
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current

LaunchedEffect(Unit) {
infoMessages.collect {
snackbarHostState.showSnackbar(
message = it.uiText.asString(context.resources)
)
}
}
}

enum class ConversationMediaScreenTabItem(@StringRes override val titleResId: Int) : TabItem {
PICTURES(R.string.label_conversation_pictures),
FILES(R.string.label_conversation_files);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ class ConversationMessagesViewModel @Inject constructor(
val assetContent = messageContent.value
assetDataPath(conversationId, messageId)?.let { (path, _) ->
messageId to AssetBundle(
key = assetContent.remoteData.assetId,
dataPath = path,
fileName = assetContent.name ?: DEFAULT_ASSET_NAME,
dataSize = assetContent.sizeInBytes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import okio.Path
* Represents a set of metadata information of an asset message
*/
data class AssetBundle(
val key: String,
val mimeType: String,
val dataPath: Path,
val dataSize: Long,
Expand Down
Loading

0 comments on commit fec0ad9

Please sign in to comment.