From 59ec9cc1840b8bddd4c0ed0e5c2c1ecbd25404a0 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:14:38 +0200 Subject: [PATCH] feat: add reply in private feature --- .../mapper/RegularMessageContentMapper.kt | 22 +++++- .../android/ui/edit/ReplyMessageOption.kt | 14 ++++ .../ConversationCoreViewModelFactory.kt | 8 +++ .../ConversationCoreViewModelGraph.kt | 5 ++ .../home/conversations/ConversationScreen.kt | 54 +++++++++++++- .../edit/AssetOptionsMenuItems.kt | 4 ++ .../edit/MessageOptionsMenuItems.kt | 8 ++- .../edit/MessageOptionsModalSheetLayout.kt | 17 +++++ .../edit/TextMessageOptionsMenuItems.kt | 4 ++ .../messages/ConversationMessagesViewModel.kt | 3 + .../conversations/messages/QuotedMessage.kt | 2 +- .../messages/draft/MessageDraftViewModel.kt | 10 ++- .../messages/preview/PreviewMessageTypes.kt | 1 + .../conversations/model/UIQuotedMessage.kt | 2 + .../privateReply/ReplyInPrivateAction.kt | 25 +++++++ .../privateReply/ReplyInPrivateViewModel.kt | 71 +++++++++++++++++++ .../sendmessage/SendMessageViewModel.kt | 25 ++++++- .../GetQuoteMessageForConversationUseCase.kt | 1 + ...serveQuoteMessageForConversationUseCase.kt | 1 + .../messagecomposer/model/MessageBundle.kt | 6 +- .../model/MessageComposition.kt | 10 ++- .../state/MessageComposerStateHolder.kt | 6 +- .../state/MessageCompositionHolder.kt | 27 ++++++- app/src/main/res/values/strings.xml | 2 + .../QuotedMultipartMessageViewModelTest.kt | 1 + .../draft/MessageDraftViewModelTest.kt | 1 + ...tQuoteMessageForConversationUseCaseTest.kt | 1 + kalium | 2 +- 28 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateAction.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt index fcd0ae41797..ca8ecf42be2 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt @@ -99,7 +99,12 @@ class RegularMessageMapper @Inject constructor( is MessageContent.Composite -> { val text = content.textContent?.let { textContent -> - val quotedMessage = textContent.quotedMessageDetails?.let { mapQuoteData(message.conversationId, it) } + val quotedMessage = textContent.quotedMessageDetails?.let { + mapQuoteData( + conversationId = textContent.quotedMessageReference?.quotedMessageConversationId ?: message.conversationId, + details = it + ) + } ?: if (textContent.quotedMessageReference?.quotedMessageId != null) { UIQuotedMessage.UnavailableData } else { @@ -169,7 +174,12 @@ class RegularMessageMapper @Inject constructor( ): UIMessageContent.TextMessage { val messageTextContent = (content as? MessageContent.Text) - val quotedMessage = messageTextContent?.quotedMessageDetails?.let { mapQuoteData(conversationId, it) } + val quotedMessage = messageTextContent?.quotedMessageDetails?.let { + mapQuoteData( + conversationId = messageTextContent.quotedMessageReference?.quotedMessageConversationId ?: conversationId, + details = it + ) + } ?: if (messageTextContent?.quotedMessageReference?.quotedMessageId != null) { UIQuotedMessage.UnavailableData } else { @@ -207,6 +217,7 @@ class RegularMessageMapper @Inject constructor( private fun mapQuoteData(conversationId: ConversationId, details: MessageContent.QuotedMessageDetails) = UIQuotedMessage.UIQuotedData( messageId = details.messageId, + conversationId = conversationId, senderId = details.senderId, senderName = details.senderName.orUnknownName(), senderAccent = Accent.fromAccentId(details.accentId), @@ -330,7 +341,12 @@ class RegularMessageMapper @Inject constructor( deliveryStatus: DeliveryStatus ): UIMessageContent.Multipart { - val quotedMessage = content.quotedMessageDetails?.let { mapQuoteData(conversationId = conversationId, details = it) } + val quotedMessage = content.quotedMessageDetails?.let { + mapQuoteData( + conversationId = content.quotedMessageReference?.quotedMessageConversationId ?: conversationId, + details = it + ) + } ?: if (content.quotedMessageReference?.quotedMessageId != null) { UIQuotedMessage.UnavailableData } else { diff --git a/app/src/main/kotlin/com/wire/android/ui/edit/ReplyMessageOption.kt b/app/src/main/kotlin/com/wire/android/ui/edit/ReplyMessageOption.kt index b0a9745d101..cb3f0d1ec9a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/edit/ReplyMessageOption.kt +++ b/app/src/main/kotlin/com/wire/android/ui/edit/ReplyMessageOption.kt @@ -36,3 +36,17 @@ fun ReplyMessageOption(onReplyItemClick: () -> Unit) { onItemClick = onReplyItemClick ) } + +@Composable +fun ReplyInPrivateMessageOption(onReplyItemClick: () -> Unit) { + MenuBottomSheetItem( + leading = { + MenuItemIcon( + id = R.drawable.ic_reply, + contentDescription = stringResource(R.string.content_description_reply_in_private_to_message), + ) + }, + title = stringResource(R.string.message_option_reply_in_private), + onItemClick = onReplyItemClick + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt index 96f56d5d8cb..6354f49b1ac 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelFactory.kt @@ -42,6 +42,7 @@ import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveRece import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.messagetypes.multipart.CellAssetRefreshHelper import com.wire.android.ui.home.conversations.model.messagetypes.multipart.MultipartAttachmentsViewModelImpl +import com.wire.android.ui.home.conversations.privateReply.ReplyInPrivateViewModel import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversationUseCase @@ -80,6 +81,7 @@ import com.wire.kalium.logic.feature.client.IsWireCellsEnabledForConversationUse import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase @@ -184,6 +186,7 @@ class ConversationCoreViewModelFactory @Inject constructor( private val getAssetMessages: GetAssetMessagesFromConversationUseCase, private val observeConversationMembersByTypes: ObserveConversationMembersByTypesUseCase, private val notifyConversationIsOpen: NotifyConversationIsOpenUseCase, + private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase, private val qualifiedIdMapper: QualifiedIdMapper, private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, private val observeReactionsForMessage: ObserveReactionsForMessageUseCase, @@ -272,6 +275,11 @@ class ConversationCoreViewModelFactory @Inject constructor( saveMessageDraft = saveMessageDraft, ) + fun replyInPrivateViewModel() = ReplyInPrivateViewModel( + getOrCreateOneToOneConversation = getOrCreateOneToOneConversation, + saveMessageDraft = saveMessageDraft, + ) + fun messageAttachmentsViewModel(savedStateHandle: SavedStateHandle) = MessageAttachmentsViewModel( savedStateHandle = savedStateHandle, handleUriAsset = handleUriAsset, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelGraph.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelGraph.kt index 8ef29950efe..05934ee7acd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelGraph.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationCoreViewModelGraph.kt @@ -43,6 +43,7 @@ import com.wire.android.ui.home.conversations.messages.item.ConversationAssetPat import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.messagetypes.multipart.MultipartAttachmentsViewModel import com.wire.android.ui.home.conversations.model.messagetypes.multipart.MultipartAttachmentsViewModelImpl +import com.wire.android.ui.home.conversations.privateReply.ReplyInPrivateViewModel import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel import com.wire.android.ui.home.gallery.MediaGalleryViewModel import com.wire.android.ui.home.messagecomposer.location.LocationPickerViewModel @@ -67,6 +68,10 @@ fun sendMessageViewModel(): SendMessageViewModel = fun messageDraftViewModel(): MessageDraftViewModel = conversationSavedStateViewModel { this.messageDraftViewModel(it) } +@Composable +fun replyInPrivateViewModel(): ReplyInPrivateViewModel = + conversationViewModel { this.replyInPrivateViewModel() } + @Composable fun messageAttachmentsViewModel(): MessageAttachmentsViewModel = conversationSavedStateViewModel { this.messageAttachmentsViewModel(it) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 91bb5c07ae7..aa1faef6247 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -177,6 +177,8 @@ import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.model.UriAsset +import com.wire.android.ui.home.conversations.privateReply.ReplyInPrivateAction +import com.wire.android.ui.home.conversations.privateReply.ReplyInPrivateViewModel import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionOptionsModalSheetLayout import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModel import com.wire.android.ui.home.gallery.MediaGalleryActionType @@ -269,10 +271,12 @@ fun ConversationScreen( conversationMigrationViewModel: ConversationMigrationViewModel = conversationMigrationViewModel(), messageDraftViewModel: MessageDraftViewModel = messageDraftViewModel(), messageAttachmentsViewModel: MessageAttachmentsViewModel = messageAttachmentsViewModel(), + replyInPrivateViewModel: ReplyInPrivateViewModel = replyInPrivateViewModel(), ) { val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current val resources = LocalContext.current.resources + val snackbarHostState = LocalSnackbarHostState.current val showDialog = remember { mutableStateOf(ConversationScreenDialogType.NONE) } val messageComposerViewState = messageComposerViewModel.messageComposerViewState val messageComposerStateHolder = rememberMessageComposerStateHolder( @@ -348,7 +352,10 @@ fun ConversationScreen( LaunchedEffect(messageDraftViewModel.state.value.quotedMessageId) { val compositionState = messageDraftViewModel.state.value if (compositionState.quotedMessage != null) { - messageComposerStateHolder.messageCompositionHolder.value.updateQuote(compositionState.quotedMessage) + messageComposerStateHolder.messageCompositionHolder.value.updateQuote( + quotedMessage = compositionState.quotedMessage, + quotedMessageConversationId = compositionState.quotedMessageConversationId + ) } } @@ -372,6 +379,21 @@ fun ConversationScreen( } } + HandleActions(replyInPrivateViewModel.actions) { action -> + when (action) { + ReplyInPrivateAction.Failure -> coroutineScope.launch { + snackbarHostState.showSnackbar(resources.getString(R.string.error_conversation_generic)) + } + + is ReplyInPrivateAction.Navigate -> navigator.navigate( + NavigationCommand( + ConversationScreenDestination(action.conversationId), + BackStackMode.UPDATE_EXISTED + ) + ) + } + } + conversationMigrationViewModel.migratedConversationId?.let { migratedConversationId -> navigator.navigate( NavigationCommand( @@ -625,6 +647,7 @@ fun ConversationScreen( onReactionClick = { messageId, emoji -> conversationMessagesViewModel.toggleReaction(messageId, emoji) }, + onReplyInPrivateClick = replyInPrivateViewModel::replyInPrivate, onResetSessionClick = conversationMessagesViewModel::onResetSession, onUpdateConversationReadDate = messageComposerViewModel::updateConversationReadDate, onDropDownClick = { @@ -671,7 +694,22 @@ fun ConversationScreen( shareAsset = conversationMessagesViewModel::shareAsset, onDownloadAssetClick = conversationMessagesViewModel::openOrFetchAsset, onOpenAssetClick = conversationMessagesViewModel::downloadAndOpenAsset, - onNavigateToReplyOriginalMessage = conversationMessagesViewModel::navigateToReplyOriginalMessage, + onNavigateToReplyOriginalMessage = { message -> + message.quotedMessageData()?.let { quotedData -> + navigator.navigate( + NavigationCommand( + ConversationScreenDestination( + navArgs = ConversationNavArgs( + conversationId = quotedData.conversationId, + searchedMessageId = quotedData.messageId + ) + ), + BackStackMode.REMOVE_CURRENT_AND_REPLACE, + launchSingleTop = false + ) + ) + } + }, onSelfDeletingMessageRead = messageComposerViewModel::startSelfDeletion, onNewSelfDeletingMessagesStatus = messageComposerViewModel::updateSelfDeletingMessages, tempWritableImageUri = messageComposerViewModel.tempWritableImageUri, @@ -977,6 +1015,7 @@ private fun ConversationScreen( onStartCall: () -> Unit, onJoinCall: () -> Unit, onReactionClick: (messageId: String, reactionEmoji: String) -> Unit, + onReplyInPrivateClick: (UIMessage.Regular) -> Unit, onResetSessionClick: (senderUserId: UserId, clientId: String?) -> Unit, onUpdateConversationReadDate: (String) -> Unit, onDropDownClick: () -> Unit, @@ -1123,6 +1162,8 @@ private fun ConversationScreen( onReactionClick = onReactionClick, onDetailsClick = onMessageDetailsClick, onReplyClick = messageComposerStateHolder::toReply, + onReplyInPrivateClick = onReplyInPrivateClick, + isGroupConversation = conversationInfoViewState.conversationType is Conversation.Type.Group, onEditClick = messageComposerStateHolder::toEdit, onShareAssetClick = { shareAsset(context, it) }, onDownloadAssetClick = onDownloadAssetClick, @@ -1943,6 +1984,14 @@ private fun CoroutineScope.withSmoothScreenLoad(block: () -> Unit) = launch { block() } +private fun UIMessage.quotedMessageData(): UIQuotedMessage.UIQuotedData? = + when (val content = messageContent) { + is UIMessageContent.TextMessage -> content.messageBody.quotedMessage as? UIQuotedMessage.UIQuotedData + is UIMessageContent.Composite -> content.messageBody?.quotedMessage as? UIQuotedMessage.UIQuotedData + is UIMessageContent.Multipart -> content.messageBody?.quotedMessage as? UIQuotedMessage.UIQuotedData + else -> null + } + enum class ConversationActionPermissionType { CaptureVideo, TakePicture, ChooseImage, ChooseFile, CallAudio } @@ -1985,6 +2034,7 @@ fun PreviewConversationScreen() = WireTheme { onStartCall = { }, onJoinCall = { }, onReactionClick = { _, _ -> }, + onReplyInPrivateClick = { }, onResetSessionClick = { _, _ -> }, onUpdateConversationReadDate = { }, onDropDownClick = { }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/AssetOptionsMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/AssetOptionsMenuItems.kt index 6fe78438314..f7771672e9b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/AssetOptionsMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/AssetOptionsMenuItems.kt @@ -23,6 +23,7 @@ import com.wire.android.ui.edit.DownloadAssetExternallyOption import com.wire.android.ui.edit.MessageDetailsMenuOption import com.wire.android.ui.edit.OpenAssetExternallyOption import com.wire.android.ui.edit.ReactionOption +import com.wire.android.ui.edit.ReplyInPrivateMessageOption import com.wire.android.ui.edit.ReplyMessageOption import com.wire.android.ui.edit.ShareAssetMenuOption @@ -36,6 +37,8 @@ fun assetMessageOptionsMenuItems( onShareAsset: () -> Unit, onDownloadAsset: () -> Unit, onReplyClick: () -> Unit, + onReplyInPrivateClick: () -> Unit, + isReplyInPrivateAllowed: Boolean, onReactionClick: (emoji: String) -> Unit, isOpenable: Boolean = false, onOpenAsset: () -> Unit = {}, @@ -58,6 +61,7 @@ fun assetMessageOptionsMenuItems( add { ReactionOption(ownReactions, onReactionClick) } add { MessageDetailsMenuOption(onDetailsClick) } add { ReplyMessageOption(onReplyClick) } + if (isReplyInPrivateAllowed) add { ReplyInPrivateMessageOption(onReplyInPrivateClick) } add { DownloadAssetExternallyOption(onDownloadAsset) } add { ShareAssetMenuOption(onShareAsset) } if (isOpenable) add { OpenAssetExternallyOption(onOpenAsset) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuItems.kt index d3ef8a11585..8a2e4736e3c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuItems.kt @@ -35,6 +35,8 @@ fun messageOptionsMenuItems( onReactionClick: (reactionEmoji: String) -> Unit, onDetailsClick: () -> Unit, onReplyClick: () -> Unit, + onReplyInPrivateClick: () -> Unit, + isReplyInPrivateAllowed: Boolean, onEditClick: () -> Unit, onShareAssetClick: () -> Unit, onDownloadAssetClick: () -> Unit, @@ -51,6 +53,8 @@ fun messageOptionsMenuItems( onShareAsset = onShareAssetClick, onDownloadAsset = onDownloadAssetClick, onReplyClick = onReplyClick, + onReplyInPrivateClick = onReplyInPrivateClick, + isReplyInPrivateAllowed = isReplyInPrivateAllowed, onReactionClick = onReactionClick, onOpenAsset = onOpenAssetClick, ) @@ -67,7 +71,9 @@ fun messageOptionsMenuItems( onReactionClick = onReactionClick, onEditClick = onEditClick, onCopyClick = onCopyClick, - onReplyClick = onReplyClick + onReplyClick = onReplyClick, + onReplyInPrivateClick = onReplyInPrivateClick, + isReplyInPrivateAllowed = isReplyInPrivateAllowed, ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt index 16aa45bdb56..4e7a486abe2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt @@ -57,6 +57,8 @@ fun MessageOptionsModalSheetLayout( onReactionClick: (messageId: String, reactionEmoji: String) -> Unit, onDetailsClick: (messageId: String, isSelfMessage: Boolean) -> Unit, onReplyClick: (UIMessage.Regular) -> Unit, + onReplyInPrivateClick: (UIMessage.Regular) -> Unit, + isGroupConversation: Boolean, onEditClick: (messageId: String, messageBody: String, mentions: List, isMultipart: Boolean) -> Unit, onShareAssetClick: (messageId: String) -> Unit, onDownloadAssetClick: (messageId: String) -> Unit, @@ -78,6 +80,8 @@ fun MessageOptionsModalSheetLayout( onReactionClick = onReactionClick, onDetailsClick = onDetailsClick, onReplyClick = onReplyClick, + onReplyInPrivateClick = onReplyInPrivateClick, + isGroupConversation = isGroupConversation, onEditClick = onEditClick, onShareAssetClick = onShareAssetClick, onDownloadAssetClick = onDownloadAssetClick, @@ -111,6 +115,8 @@ private fun MessageOptionsModalContent( onReactionClick: (messageId: String, reactionEmoji: String) -> Unit, onDetailsClick: (messageId: String, isSelfMessage: Boolean) -> Unit, onReplyClick: (UIMessage.Regular) -> Unit, + onReplyInPrivateClick: (UIMessage.Regular) -> Unit, + isGroupConversation: Boolean, onEditClick: (messageId: String, messageBody: String, mentions: List, isMultipart: Boolean) -> Unit, onShareAssetClick: (messageId: String) -> Unit, onDownloadAssetClick: (messageId: String) -> Unit, @@ -121,6 +127,7 @@ private fun MessageOptionsModalContent( val isDeleted = message.isDeleted val isMyMessage = message.isMyMessage val isEphemeral = message.header.messageStatus.expirationStatus is ExpirationStatus.Expirable + val isReplyInPrivateAllowed = isGroupConversation && message.isReplyable && !isMyMessage && message.header.userId != null WireMenuModalSheetContent( header = MenuModalSheetHeader.Gone, menuItems = messageOptionsMenuItems( @@ -172,6 +179,14 @@ private fun MessageOptionsModalContent( } } }, + onReplyInPrivateClick = remember(message.header.messageId) { + { + sheetState.hide { + onReplyInPrivateClick(message) + } + } + }, + isReplyInPrivateAllowed = isReplyInPrivateAllowed, onEditClick = remember(message.header.messageId, message.messageContent) { { when (message.messageContent) { @@ -255,6 +270,8 @@ fun PreviewMessageOptionsModalSheetLayout() = WireTheme { onReactionClick = { _, _ -> }, onDetailsClick = { _, _ -> }, onReplyClick = { }, + onReplyInPrivateClick = { }, + isGroupConversation = true, onEditClick = { _, _, _, _ -> }, onShareAssetClick = { }, onDownloadAssetClick = { }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageOptionsMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageOptionsMenuItems.kt index 634fb4542f7..1f653f36944 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageOptionsMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageOptionsMenuItems.kt @@ -23,6 +23,7 @@ import com.wire.android.ui.edit.DeleteItemMenuOption import com.wire.android.ui.edit.EditMessageMenuOption import com.wire.android.ui.edit.MessageDetailsMenuOption import com.wire.android.ui.edit.ReactionOption +import com.wire.android.ui.edit.ReplyInPrivateMessageOption import com.wire.android.ui.edit.ReplyMessageOption @Composable @@ -36,6 +37,8 @@ fun textMessageEditMenuItems( onDeleteClick: () -> Unit, onDetailsClick: () -> Unit, onReplyClick: () -> Unit, + onReplyInPrivateClick: () -> Unit, + isReplyInPrivateAllowed: Boolean, onCopyClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, onEditClick: (() -> Unit), @@ -46,6 +49,7 @@ fun textMessageEditMenuItems( add { MessageDetailsMenuOption(onDetailsClick) } if (isCopyable) { add { CopyItemMenuOption(onCopyClick) } } if (!isEphemeral && !isComposite) add { ReplyMessageOption(onReplyClick) } + if (isReplyInPrivateAllowed) add { ReplyInPrivateMessageOption(onReplyInPrivateClick) } if (isEditable) add { EditMessageMenuOption(onEditClick) } } add { DeleteItemMenuOption(onDeleteClick) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index 0e83dcf2c65..fbcf13f91d6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -169,6 +169,9 @@ class ConversationMessagesViewModel( is UIMessageContent.TextMessage -> (content.messageBody.quotedMessage as UIQuotedMessage.UIQuotedData).messageId + is UIMessageContent.Composite -> + (content.messageBody?.quotedMessage as? UIQuotedMessage.UIQuotedData)?.messageId + is UIMessageContent.Multipart -> (content.messageBody?.quotedMessage as? UIQuotedMessage.UIQuotedData)?.messageId diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt index aca67b92379..6a4ffd71e6d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt @@ -186,7 +186,7 @@ internal fun QuotedMessage( ) is UIQuotedMessage.UIQuotedData.Multipart -> QuotedMultipartMessage( - conversationId = conversationId, + conversationId = messageData.conversationId, quotedMessageId = messageData.messageId, senderName = messageData.senderName, originalDateTimeText = messageData.originalMessageDateDescription, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt index 43173c8eedb..f20c96213e2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.model.toUiMention @@ -28,7 +29,6 @@ import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversa import com.wire.android.ui.home.messagecomposer.model.MessageComposition import com.wire.android.ui.home.messagecomposer.model.toDraft import com.wire.android.ui.home.messagecomposer.model.update -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.draft.MessageDraft @@ -60,6 +60,7 @@ class MessageDraftViewModel( messageComposition.copy( quotedMessageId = null, quotedMessage = null, + quotedMessageConversationId = null, draftText = String.EMPTY, ) } @@ -70,9 +71,11 @@ class MessageDraftViewModel( private fun loadMessageDraft() = viewModelScope.launch { getMessageDraft(conversationId)?.let { draft -> - val quotedMessage = draft.quotedMessageId?.let { quotedMessageId -> - getQuotedMessage(conversationId, quotedMessageId) + getQuotedMessage( + conversationId = draft.quotedMessageConversationId ?: conversationId, + quotedMessageId = quotedMessageId + ) } state.update { messageComposition -> @@ -83,6 +86,7 @@ class MessageDraftViewModel( isMultipart = draft.isMultipartEdit, quotedMessage = quotedMessage as? UIQuotedMessage.UIQuotedData, quotedMessageId = (quotedMessage as? UIQuotedMessage.UIQuotedData)?.messageId, + quotedMessageConversationId = draft.quotedMessageConversationId ?: conversationId, ) } } ?: run { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/preview/PreviewMessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/preview/PreviewMessageTypes.kt index 76bd886709a..7c34f359ae3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/preview/PreviewMessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/preview/PreviewMessageTypes.kt @@ -111,6 +111,7 @@ fun PreviewMessageWithReply() { message = UIText.DynamicString("Sure, go ahead!"), quotedMessage = UIQuotedMessage.UIQuotedData( messageId = "asdoij", + conversationId = mockMessageWithText.conversationId, senderId = previewUserId, senderName = UIText.DynamicString("John Doe"), originalMessageDateDescription = UIText.StringResource(R.string.label_quote_original_message_date, "10:30"), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt index d0392e58a11..d7542807fe3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIQuotedMessage.kt @@ -22,6 +22,7 @@ import com.wire.android.model.ImageAsset import com.wire.android.ui.theme.Accent import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.asset.AssetTransferStatus +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.CellAssetContent import com.wire.kalium.logic.data.user.UserId import kotlinx.serialization.Serializable @@ -35,6 +36,7 @@ sealed class UIQuotedMessage { @Serializable data class UIQuotedData( val messageId: String, + val conversationId: ConversationId, val senderId: UserId, val senderName: UIText, val senderAccent: Accent, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateAction.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateAction.kt new file mode 100644 index 00000000000..367357fc860 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateAction.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2026 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.privateReply + +import com.wire.kalium.logic.data.id.ConversationId + +sealed interface ReplyInPrivateAction { + data class Navigate(val conversationId: ConversationId) : ReplyInPrivateAction + data object Failure : ReplyInPrivateAction +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateViewModel.kt new file mode 100644 index 00000000000..6449b56379f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/privateReply/ReplyInPrivateViewModel.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2026 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.privateReply + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.ui.home.conversations.model.UIMessage +import com.wire.kalium.logic.data.message.draft.MessageDraft +import com.wire.kalium.logic.feature.conversation.CreateConversationResult +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class ReplyInPrivateViewModel( + private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase, + private val saveMessageDraft: SaveMessageDraftUseCase, +) : ViewModel() { + + private val _actions = MutableSharedFlow() + val actions = _actions.asSharedFlow() + + fun replyInPrivate(message: UIMessage.Regular) { + val senderId = message.header.userId ?: return emitFailure() + + viewModelScope.launch { + when (val result = getOrCreateOneToOneConversation(senderId)) { + is CreateConversationResult.Success -> { + val oneToOneConversationId = result.conversation.id + saveMessageDraft( + MessageDraft( + conversationId = oneToOneConversationId, + text = "", + editMessageId = null, + quotedMessageId = message.header.messageId, + selectedMentionList = emptyList(), + quotedMessageConversationId = message.conversationId, + ) + ) + _actions.emit(ReplyInPrivateAction.Navigate(oneToOneConversationId)) + } + + is CreateConversationResult.Failure -> { + _actions.emit(ReplyInPrivateAction.Failure) + } + } + } + } + + private fun emitFailure() { + viewModelScope.launch { + _actions.emit(ReplyInPrivateAction.Failure) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt index 1ae9dd7a0a7..6182470738a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt @@ -57,6 +57,7 @@ import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.failure.LegalHoldEnabledForConversationFailure import com.wire.kalium.logic.feature.asset.upload.AssetUploadParams import com.wire.kalium.logic.feature.asset.upload.ScheduleNewAssetMessageResult @@ -267,7 +268,7 @@ class SendMessageViewModel( conversationId = conversationId, text = message, mentions = mentions.map { it.intoMessageMention() }, - quotedMessageId = quotedMessageId + quotedMessageReference = quotedMessageReference() ).toEither() .handleLegalHoldFailureAfterSendingMessage(conversationId) .handleNonAssetContributionEvent(messageBundle) @@ -282,7 +283,7 @@ class SendMessageViewModel( conversationId = conversationId, text = message, mentions = mentions.map { it.intoMessageMention() }, - quotedMessageId = quotedMessageId + quotedMessageReference = quotedMessageReference() ).toEither() .handleLegalHoldFailureAfterSendingMessage(conversationId) .handleNonAssetContributionEvent(messageBundle) @@ -308,6 +309,26 @@ class SendMessageViewModel( } } + private fun ComposableMessageBundle.SendTextMessageBundle.quotedMessageReference(): MessageContent.QuoteReference? = + quotedMessageId?.let { + MessageContent.QuoteReference( + quotedMessageId = it, + quotedMessageConversationId = quotedMessageConversationId, + quotedMessageSha256 = null, + isVerified = true + ) + } + + private fun ComposableMessageBundle.SendMultipartMessageBundle.quotedMessageReference(): MessageContent.QuoteReference? = + quotedMessageId?.let { + MessageContent.QuoteReference( + quotedMessageId = it, + quotedMessageConversationId = quotedMessageConversationId, + quotedMessageSha256 = null, + isVerified = true + ) + } + private suspend fun handleAssetMessageBundle( conversationId: ConversationId, attachmentUri: UriAsset diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt index 577e7454a1a..e7351ceb992 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt @@ -49,6 +49,7 @@ class GetQuoteMessageForConversationUseCase @Inject constructor( uiMessage.header.userId?.let { senderId -> UIQuotedMessage.UIQuotedData( messageId = uiMessage.header.messageId, + conversationId = conversationId, senderId = senderId, senderName = uiMessage.header.username, originalMessageDateDescription = "".toUIText(), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt index 4f93b06096e..ff95528ca63 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt @@ -52,6 +52,7 @@ class ObserveQuoteMessageForConversationUseCase @Inject constructor( uiMessage.header.userId?.let { senderId -> UIQuotedMessage.UIQuotedData( messageId = uiMessage.header.messageId, + conversationId = conversationId, senderId = senderId, senderName = uiMessage.header.username, originalMessageDateDescription = "".toUIText(), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt index eb6898e8ac4..78b94d87f92 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageBundle.kt @@ -44,14 +44,16 @@ sealed class ComposableMessageBundle(override val conversationId: ConversationId override val conversationId: ConversationId, val message: String, val mentions: List, - val quotedMessageId: String? = null + val quotedMessageId: String? = null, + val quotedMessageConversationId: ConversationId? = null ) : ComposableMessageBundle(conversationId) data class SendMultipartMessageBundle( override val conversationId: ConversationId, val message: String, val mentions: List, - val quotedMessageId: String? = null + val quotedMessageId: String? = null, + val quotedMessageConversationId: ConversationId? = null ) : ComposableMessageBundle(conversationId) data class AttachmentPickedBundle( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageComposition.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageComposition.kt index 03600e010e7..e5b369009ef 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageComposition.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/model/MessageComposition.kt @@ -31,6 +31,7 @@ data class MessageComposition( val editMessageId: String? = null, val quotedMessage: UIQuotedMessage.UIQuotedData? = null, val quotedMessageId: String? = null, + val quotedMessageConversationId: ConversationId? = null, val selectedMentions: List = emptyList(), val isMultipart: Boolean = false, ) { @@ -86,14 +87,16 @@ data class MessageComposition( conversationId = conversationId, message = messageText, mentions = selectedMentions, - quotedMessageId = quotedMessageId + quotedMessageId = quotedMessageId, + quotedMessageConversationId = quotedMessageConversationId ) } else { ComposableMessageBundle.SendMultipartMessageBundle( conversationId = conversationId, message = messageText, mentions = selectedMentions, - quotedMessageId = quotedMessageId + quotedMessageId = quotedMessageId, + quotedMessageConversationId = quotedMessageConversationId ) } } @@ -111,6 +114,7 @@ fun MessageComposition.toDraft(messageText: String): MessageDraft { text = messageText, editMessageId = editMessageId, quotedMessageId = quotedMessageId, - selectedMentionList = selectedMentions.map { it.intoMessageMention() } + selectedMentionList = selectedMentions.map { it.intoMessageMention() }, + quotedMessageConversationId = quotedMessageConversationId ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt index dcf870759ef..fc293b6b967 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageComposerStateHolder.kt @@ -51,7 +51,7 @@ fun rememberMessageComposerStateHolder( ): MessageComposerStateHolder { val density = LocalDensity.current - val messageComposition = remember(draftMessageComposition) { + val messageComposition = remember { mutableStateOf(draftMessageComposition) } @@ -72,7 +72,9 @@ fun rememberMessageComposerStateHolder( ) } - LaunchedEffect(draftMessageComposition.draftText) { + LaunchedEffect(draftMessageComposition) { + messageComposition.value = draftMessageComposition + if (draftMessageComposition.draftText.isNotBlank()) { messageTextState.setTextAndPlaceCursorAtEnd(draftMessageComposition.draftText) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt index c71c1c35216..0fec01eb9dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/state/MessageCompositionHolder.kt @@ -67,11 +67,17 @@ class MessageCompositionHolder( const val RICH_TEXT_MARKDOWN_MULTIPLIER = 2 } - fun updateQuote(quotedMessage: UIQuotedMessage.UIQuotedData) { + private var hasObservedTextUpdate = false + + fun updateQuote( + quotedMessage: UIQuotedMessage.UIQuotedData, + quotedMessageConversationId: ConversationId? = messageComposition.value.conversationId + ) { messageComposition.update { it.copy( quotedMessage = quotedMessage, quotedMessageId = quotedMessage.messageId, + quotedMessageConversationId = quotedMessageConversationId, editMessageId = null ) } @@ -84,6 +90,7 @@ class MessageCompositionHolder( message.mapToQuotedContent()?.let { quotedContent -> val quotedMessage = UIQuotedMessage.UIQuotedData( messageId = message.header.messageId, + conversationId = message.conversationId, senderId = senderId, senderName = message.header.username, originalMessageDateDescription = "".toUIText(), @@ -96,6 +103,7 @@ class MessageCompositionHolder( it.copy( quotedMessage = quotedMessage, quotedMessageId = message.header.messageId, + quotedMessageConversationId = message.conversationId, editMessageId = null ) } @@ -107,7 +115,8 @@ class MessageCompositionHolder( messageComposition.update { it.copy( quotedMessage = null, - quotedMessageId = null + quotedMessageId = null, + quotedMessageConversationId = null ) } onSaveDraft(messageComposition.value.toDraft(String.EMPTY)) @@ -120,10 +129,21 @@ class MessageCompositionHolder( updateTypingEvent(messageText.toString()) updateMentionsIfNeeded(messageText.toString()) requestMentionSuggestionIfNeeded(messageText.toString(), selection) - onMessageTextUpdate(messageComposition.value.toDraft(messageText = messageText.toString())) + val draft = messageComposition.value.toDraft(messageText = messageText.toString()) + if (!draft.isEmpty() || hasObservedTextUpdate) { + onMessageTextUpdate(draft) + } + hasObservedTextUpdate = true } } + private fun MessageDraft.isEmpty(): Boolean = + text.isEmpty() && + editMessageId == null && + quotedMessageId == null && + quotedMessageConversationId == null && + selectedMentionList.isEmpty() + private fun updateMentionsIfNeeded(messageText: String) { messageComposition.update { it.copy(selectedMentions = it.getSelectedMentions(messageText)) } } @@ -316,6 +336,7 @@ class MessageCompositionHolder( it.copy( quotedMessageId = null, quotedMessage = null, + quotedMessageConversationId = null, editMessageId = null, selectedMentions = emptyList() ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 155fc7c078e..97b73657ef8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,6 +178,7 @@ View details Close details Reply to the message + Reply to the message in private Cancel message reply Ping message Copy @@ -1114,6 +1115,7 @@ Wants to connect Deleted the conversation Reply + Reply in private You Type a message Open Call diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelTest.kt index a6aced81f7d..eed3ab371f4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelTest.kt @@ -132,6 +132,7 @@ class QuotedMultipartMessageViewModelTest { } returns flowOf( UIQuotedMessage.UIQuotedData( messageId = "message_id", + conversationId = QualifiedID("conversation", "domain"), senderId = UserId("user", "domain"), senderName = UIText.DynamicString("Sender Name"), senderAccent = Accent.Unknown, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt index ca31d42e995..f9484f8ead3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt @@ -103,6 +103,7 @@ class MessageDraftViewModelTest { ) val quotedData = UIQuotedMessage.UIQuotedData( messageId = "quoted_message_id", + conversationId = TestConversation.ID, senderId = UserId("user_id", "domain"), senderName = UIText.DynamicString("John"), originalMessageDateDescription = UIText.StringResource(R.string.label_quote_original_message_date, "10:30"), diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCaseTest.kt index 3501bd88fe0..8a6026675ff 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCaseTest.kt @@ -81,6 +81,7 @@ class GetQuoteMessageForConversationUseCaseTest { assertEquals( UIQuotedMessage.UIQuotedData( messageId = "message_id", + conversationId = CONVERSATION_ID, senderId = USER_ID, senderName = "User".toUIText(), senderAccent = Accent.Unknown, diff --git a/kalium b/kalium index 09e868d171c..ca5cd5f080b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 09e868d171cb71f123a69afd90fd07c6c488bdf8 +Subproject commit ca5cd5f080b2b6e01618cc4d90b8ba7445e6639c