diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 0a91750ca5e..c132c1d72d8 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -89,6 +89,7 @@ import com.wire.kalium.logic.feature.message.SendEditTextMessageUseCase import com.wire.kalium.logic.feature.message.SendKnockUseCase import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.message.ToggleReactionUseCase +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase @@ -1208,6 +1209,14 @@ class UseCaseModule { ): DeleteAccountUseCase = coreLogic.getSessionScope(currentAccount).users.deleteAccount + @ViewModelScoped + @Provides + fun provideSendButtonActionMessageUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ): SendButtonActionMessageUseCase = + coreLogic.getSessionScope(currentAccount).messages.sendButtonActionMessage + @ViewModelScoped @Provides fun providePersistScreenshotCensoringConfigUseCase( diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessagePreviewContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessagePreviewContentMapper.kt index 7b6d53458df..7dfd6c67945 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessagePreviewContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessagePreviewContentMapper.kt @@ -247,10 +247,20 @@ fun MessagePreview.uiLastMessageContent(): UILastMessageContent { is WithUser.TeamMemberRemoved -> UILastMessageContent.None // TODO is WithUser.Text -> UILastMessageContent.SenderWithMessage( sender = userUIText, - message = UIText.DynamicString((content as WithUser.Text).messageBody), + message = (content as WithUser.Text).messageBody.let { UIText.DynamicString(it) }, separator = ": " ) + is WithUser.Composite -> { + val text = (content as WithUser.Composite).messageBody?.let { UIText.DynamicString(it) } + ?: UIText.StringResource(R.string.last_message_composite_with_missing_text) + UILastMessageContent.SenderWithMessage( + sender = userUIText, + message = text, + separator = ": " + ) + } + is WithUser.MissedCall -> UILastMessageContent.TextMessage( MessageBody(UIText.PluralResource(R.plurals.unread_event_call, 1, 1)) ) 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 681612564c9..4acf1d9c2c9 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/RegularMessageContentMapper.kt @@ -25,6 +25,7 @@ import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversations.findUser import com.wire.android.ui.home.conversations.model.DeliveryStatusContent import com.wire.android.ui.home.conversations.model.MessageBody +import com.wire.android.ui.home.conversations.model.MessageButton import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.util.time.ISOFormatter @@ -91,6 +92,33 @@ class RegularMessageMapper @Inject constructor( message.deliveryStatus ) + is MessageContent.Composite -> { + val text = content.textContent?.let { textContent -> + val quotedMessage = textContent.quotedMessageDetails?.let { mapQuoteData(message.conversationId, it) } + ?: if (textContent.quotedMessageReference?.quotedMessageId != null) { + UIQuotedMessage.UnavailableData + } else { + null + } + + MessageBody( + message = UIText.DynamicString(textContent.value, content.textContent?.mentions.orEmpty()), + quotedMessage = quotedMessage + ) + } + + UIMessageContent.Composite( + messageBody = text, + buttonList = content.buttonList.map { + MessageButton( + id = it.id, + text = it.text, + isSelected = it.isSelected + ) + } + ) + } + else -> toText(message.conversationId, content, userList, message.deliveryStatus) } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt index 19cdabbfd21..ae7d2fac597 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt @@ -45,7 +45,7 @@ data class Device( clientId = client.id, registrationTime = client.registrationTime?.toIsoDateTimeString(), lastActiveInWholeWeeks = client.lastActiveInWholeWeeks(), - isValid = client.isVerified, + isValid = client.isValid, isVerified = client.isVerified ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt new file mode 100644 index 00000000000..57055aedce3 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2023 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 + +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.navigation.EXTRA_CONVERSATION_ID +import com.wire.android.navigation.EXTRA_MESSAGE_ID +import com.wire.kalium.logic.data.id.MessageButtonId +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CompositeMessageViewModel @Inject constructor( + private val sendButtonActionMessageUseCase: SendButtonActionMessageUseCase, + qualifiedIdMapper: QualifiedIdMapper, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + val conversationId: QualifiedID = qualifiedIdMapper.fromStringToQualifiedID( + savedStateHandle.get(EXTRA_CONVERSATION_ID)!! + ) + + private val messageId: String = savedStateHandle.get(EXTRA_MESSAGE_ID)!! + + var pendingButtonId: MessageButtonId? by mutableStateOf(null) + @VisibleForTesting + set + + fun sendButtonActionMessage(buttonId: String) { + if (pendingButtonId != null) return + + pendingButtonId = buttonId + viewModelScope.launch { + sendButtonActionMessageUseCase(conversationId, messageId, buttonId) + }.invokeOnCompletion { + pendingButtonId = null + } + } + + companion object { + const val ARGS_KEY = "CompositeMessageViewModelKey" + } +} 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 74413860189..90b6874b0df 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 @@ -118,7 +118,7 @@ fun ConversationScreen( conversationBannerViewModel: ConversationBannerViewModel = hiltSavedStateViewModel(backNavArgs = backNavArgs), conversationCallViewModel: ConversationCallViewModel = hiltSavedStateViewModel(backNavArgs = backNavArgs), conversationMessagesViewModel: ConversationMessagesViewModel = hiltSavedStateViewModel(backNavArgs = backNavArgs), - messageComposerViewModel: MessageComposerViewModel = hiltSavedStateViewModel(backNavArgs = backNavArgs) + messageComposerViewModel: MessageComposerViewModel = hiltSavedStateViewModel(backNavArgs = backNavArgs), ) { val coroutineScope = rememberCoroutineScope() val showDialog = remember { mutableStateOf(ConversationScreenDialogType.NONE) } @@ -421,7 +421,7 @@ private fun ConversationScreen( onClearMentionSearchResult = onClearMentionSearchResult, tempWritableImageUri = tempWritableImageUri, tempWritableVideoUri = tempWritableVideoUri, - snackBarHostState = conversationScreenState.snackBarHostState + snackBarHostState = conversationScreenState.snackBarHostState, ) } MenuModalSheetLayout( @@ -491,7 +491,7 @@ private fun ConversationScreenContent( onShowEditingOption = onShowEditingOptions, conversationDetailsData = conversationDetailsData, onFailedMessageCancelClicked = onFailedMessageCancelClicked, - onFailedMessageRetryClicked = onFailedMessageRetryClicked + onFailedMessageRetryClicked = onFailedMessageRetryClicked, ) }, onChangeSelfDeletionClicked = onChangeSelfDeletionClicked, @@ -571,7 +571,7 @@ fun MessageList( onSelfDeletingMessageRead: (UIMessage) -> Unit, conversationDetailsData: ConversationDetailsData, onFailedMessageRetryClicked: (String) -> Unit, - onFailedMessageCancelClicked: (String) -> Unit + onFailedMessageCancelClicked: (String) -> Unit, ) { val mostRecentMessage = lazyPagingMessages.itemCount.takeIf { it > 0 }?.let { lazyPagingMessages[0] } @@ -627,7 +627,7 @@ fun MessageList( onResetSessionClicked = onResetSessionClicked, onSelfDeletingMessageRead = onSelfDeletingMessageRead, onFailedMessageCancelClicked = onFailedMessageCancelClicked, - onFailedMessageRetryClicked = onFailedMessageRetryClicked + onFailedMessageRetryClicked = onFailedMessageRetryClicked, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index b74bcb32799..3ad6e8c3d1e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -103,7 +103,7 @@ fun MessageItem( onResetSessionClicked: (senderUserId: UserId, clientId: String?) -> Unit, onSelfDeletingMessageRead: (UIMessage) -> Unit, onFailedMessageRetryClicked: (String) -> Unit = {}, - onFailedMessageCancelClicked: (String) -> Unit = {} + onFailedMessageCancelClicked: (String) -> Unit = {}, ) { with(message) { val selfDeletionTimerState = rememberSelfDeletionTimer(header.messageStatus.expirationStatus) @@ -158,7 +158,7 @@ fun MessageItem( val isProfileRedirectEnabled = header.userId != null && - !(header.isSenderDeleted || header.isSenderUnavailable) + !(header.isSenderDeleted || header.isSenderUnavailable) if (showAuthor) { val avatarClickable = remember { @@ -229,7 +229,7 @@ fun MessageItem( onAssetClick = currentOnAssetClicked, onImageClick = currentOnImageClick, onLongClick = onLongClick, - onOpenProfile = onOpenProfile + onOpenProfile = onOpenProfile, ) } if (isMyMessage) { @@ -475,12 +475,35 @@ private fun MessageContent( messageBody = messageContent.messageBody, isAvailable = !message.isPending && message.isAvailable, onLongClick = onLongClick, - onOpenProfile = onOpenProfile + onOpenProfile = onOpenProfile, + buttonList = null, + messageId = message.header.messageId ) PartialDeliveryInformation(messageContent.deliveryStatus) } } + is UIMessageContent.Composite -> { + Column { + messageContent.messageBody?.quotedMessage?.let { + VerticalSpace.x4() + when (it) { + is UIQuotedMessage.UIQuotedData -> QuotedMessage(it) + UIQuotedMessage.UnavailableData -> QuotedUnavailable(QuotedMessageStyle.COMPLETE) + } + VerticalSpace.x4() + } + MessageBody( + messageBody = messageContent.messageBody, + isAvailable = !message.isPending && message.isAvailable, + onLongClick = onLongClick, + onOpenProfile = onOpenProfile, + buttonList = messageContent.buttonList, + messageId = message.header.messageId, + ) + } + } + is UIMessageContent.AssetMessage -> { MessageGenericAsset( assetName = messageContent.assetName, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt index a0a3182946f..985691b4947 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/EditMessageMenuItems.kt @@ -30,6 +30,7 @@ import com.wire.android.ui.common.bottomsheet.MenuItemIcon 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.model.UIMessageContent +import com.wire.android.util.Copyable import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.message.mention.MessageMention @@ -49,39 +50,40 @@ fun EditMessageMenuItems( ): List<@Composable () -> Unit> { val localContext = LocalContext.current - val onCopyItemClick = remember(message) { - { - hideEditMessageMenu { - onCopyClick( - (message.messageContent as UIMessageContent.TextMessage).messageBody.message.asString( - localContext.resources - ) - ) + val isComposite = remember(message.header.messageId) { + message.messageContent is UIMessageContent.Composite + } + + val onCopyItemClick: (() -> Unit)? = remember(message.header.messageId) { + (message.messageContent as? Copyable)?.textToCopy(localContext.resources)?.let { + { + hideEditMessageMenu { onCopyClick(it) } } } } - val onDeleteItemClick = remember(message) { + + val onDeleteItemClick = remember(message.header.messageId) { { hideEditMessageMenu { onDeleteClick(message.header.messageId, message.isMyMessage) } } } - val onReactionItemClick = remember(message) { + val onReactionItemClick = remember(message.header.messageId) { { emoji: String -> hideEditMessageMenu { onReactionClick(message.header.messageId, emoji) } } } - val onReplyItemClick = remember(message) { + val onReplyItemClick = remember(message.header.messageId) { { hideEditMessageMenu { onReplyClick(message) } } } - val onDetailsItemClick = remember(message) { + val onDetailsItemClick = remember(message.header.messageId) { { hideEditMessageMenu { onDetailsClick(message.header.messageId, message.isMyMessage) @@ -132,6 +134,7 @@ fun EditMessageMenuItems( TextMessageEditMenuItems( isEphemeral = message.header.messageStatus.expirationStatus is ExpirationStatus.Expirable, isUploading = message.isPending, + isComposite = isComposite, onDeleteClick = onDeleteItemClick, onDetailsClick = onDetailsItemClick, onReactionClick = onReactionItemClick, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageMenuItems.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageMenuItems.kt index 923d2f44402..03c4d7f26d7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageMenuItems.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/TextMessageMenuItems.kt @@ -27,19 +27,20 @@ import com.wire.android.ui.edit.ReplyMessageOption fun TextMessageEditMenuItems( isEphemeral: Boolean, isUploading: Boolean, + isComposite: Boolean, onDeleteClick: () -> Unit, onDetailsClick: () -> Unit, onReplyClick: () -> Unit, - onCopyClick: () -> Unit, + onCopyClick: (() -> Unit)?, onReactionClick: (String) -> Unit, onEditClick: (() -> Unit)? = null ): List<@Composable () -> Unit> { return buildList { if (!isUploading) { - if (!isEphemeral) add { ReactionOption(onReactionClick) } + if (!isEphemeral && !isComposite) add { ReactionOption(onReactionClick) } add { MessageDetailsMenuOption(onDetailsClick) } - if (!isEphemeral) add { CopyItemMenuOption(onCopyClick) } - if (!isEphemeral) add { ReplyMessageOption(onReplyClick) } + onCopyClick?.also { add { CopyItemMenuOption(it) } } + if (!isEphemeral && !isComposite) add { ReplyMessageOption(onReplyClick) } if (!isEphemeral && onEditClick != null) 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 56303ae4349..6a0c72d26b7 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 @@ -43,13 +43,13 @@ import com.wire.android.navigation.getBackNavArg import com.wire.android.ui.home.conversations.ConversationSnackbarMessages import com.wire.android.ui.home.conversations.ConversationSnackbarMessages.OnResetSession import com.wire.android.ui.home.conversations.model.AssetBundle -import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.startFileShareIntent import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -68,6 +68,7 @@ import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -96,6 +97,7 @@ class ConversationMessagesViewModel @Inject constructor( ) : SavedStateViewModel(savedStateHandle) { var conversationViewState by mutableStateOf(ConversationMessagesViewState()) + private set private val conversationId: QualifiedID = qualifiedIdMapper.fromStringToQualifiedID( savedStateHandle.get(EXTRA_CONVERSATION_ID)!! diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt index a55234ff2aa..e8877d6ae52 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt @@ -26,19 +26,28 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import com.sebaslogen.resaca.hilt.hiltViewModelScoped import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset +import com.wire.android.navigation.EXTRA_MESSAGE_ID +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dimensions +import com.wire.android.ui.home.conversations.CompositeMessageViewModel import com.wire.android.ui.home.conversations.model.messagetypes.asset.MessageAsset import com.wire.android.ui.home.conversations.model.messagetypes.image.DisplayableImageMessage import com.wire.android.ui.home.conversations.model.messagetypes.image.ImageMessageFailed @@ -68,12 +77,16 @@ import org.commonmark.parser.Parser // waiting for the backend to implement mapping logic for the MessageBody @Composable internal fun MessageBody( - messageBody: MessageBody, + messageId: String, + messageBody: MessageBody?, isAvailable: Boolean, onLongClick: (() -> Unit)? = null, onOpenProfile: (String) -> Unit, + buttonList: List? ) { - val (displayMentions, text) = mapToDisplayMentions(messageBody.message, LocalContext.current.resources) + val (displayMentions, text) = messageBody?.message?.let { + mapToDisplayMentions(it, LocalContext.current.resources) + } ?: Pair(emptyList(), null) val nodeData = NodeData( modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x), @@ -90,8 +103,59 @@ internal fun MessageBody( StrikethroughExtension.builder().requireTwoTildes(true).build(), TablesExtension.create() ) + text?.also { + MarkdownDocument(Parser.builder().extensions(extensions).build().parse(it) as Document, nodeData) + } + buttonList?.also { + MessageButtonsContent( + messageId = messageId, + buttonList = it, + ) + } +} + +@Composable +fun MessageButtonsContent( + messageId: String, + buttonList: List, +) { + val viewModel = hiltViewModelScoped( + key = "${CompositeMessageViewModel.ARGS_KEY}$messageId", + defaultArguments = bundleOf( + EXTRA_MESSAGE_ID to messageId, + ) + ) + Column( + modifier = Modifier + .wrapContentSize() + ) { + for (index in buttonList.indices) { + val button = buttonList[index] + val onCLick = remember(button.isSelected) { + if (!button.isSelected) { + { viewModel.sendButtonActionMessage(button.id) } + } else { + { } + } + } + + val isPending = viewModel.pendingButtonId == button.id + + val state = if (button.isSelected) WireButtonState.Selected + else if (viewModel.pendingButtonId != null) WireButtonState.Disabled + else WireButtonState.Default - MarkdownDocument(Parser.builder().extensions(extensions).build().parse(text) as Document, nodeData) + WireSecondaryButton( + loading = isPending, + text = button.text, + onClick = onCLick, + state = state + ) + if (index != buttonList.lastIndex) { + Spacer(modifier = Modifier.padding(top = dimensions().spacing8x)) + } + } + } } @OptIn(ExperimentalFoundationApi::class) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 68de7c965d1..cd722eb4eae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations.model +import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.runtime.Stable @@ -28,6 +29,7 @@ import com.wire.android.model.ImageAsset import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration +import com.wire.android.util.Copyable import com.wire.android.util.ui.UIText import com.wire.android.util.uiMessageDateTime import com.wire.kalium.logic.data.conversation.ClientId @@ -211,7 +213,16 @@ sealed class UIMessageContent { data class TextMessage( val messageBody: MessageBody, override val deliveryStatus: DeliveryStatusContent = DeliveryStatusContent.CompleteDelivery - ) : Regular(), PartialDeliverable + ) : Regular(), PartialDeliverable, Copyable { + override fun textToCopy(resources: Resources): String = messageBody.message.asString(resources) + } + + data class Composite( + val messageBody: MessageBody?, + val buttonList: List + ) : Regular(), Copyable { + override fun textToCopy(resources: Resources): String? = messageBody?.message?.asString(resources) + } object Deleted : Regular() @@ -474,3 +485,10 @@ sealed interface DeliveryStatusContent { object CompleteDelivery : DeliveryStatusContent } + +@Stable +data class MessageButton( + val id: String, + val text: String, + val isSelected: Boolean, +) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt index 3ebcde064c3..285f86f010c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/LastMessageSubtitle.kt @@ -20,8 +20,8 @@ package com.wire.android.ui.home.conversationslist.common -import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow diff --git a/app/src/main/kotlin/com/wire/android/util/Copyable.kt b/app/src/main/kotlin/com/wire/android/util/Copyable.kt new file mode 100644 index 00000000000..86c6ad03b4d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/Copyable.kt @@ -0,0 +1,29 @@ +/* + * Wire + * Copyright (C) 2023 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.util + +import android.content.res.Resources + +/** + * Interface for classes that can be copied to the clipboard. + * @see [TextMessageEditMenuItems] + * if a UIMessage implement this interface the copy option will be displayed in the edit menu. + */ +interface Copyable { + fun textToCopy(resources: Resources): String? +} diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt index 5373e9acb64..da6fdf10b43 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -155,7 +155,7 @@ sealed class CurrentScreen { object InBackground : CurrentScreen() companion object { - val qualifiedIdMapper = QualifiedIdMapperImpl(null) + private val qualifiedIdMapper = QualifiedIdMapperImpl(null) @Suppress("ComplexMethod") fun fromNavigationItem(currentItem: NavigationItem?, arguments: Bundle?, isAppVisible: Boolean): CurrentScreen { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21fdf7d3a6f..4db523e2d7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1130,4 +1130,6 @@ Recording Stopped File size for audio messages is limited to %1$d MB. You can’t record an audio message during a call. + + sent an interactive message diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt new file mode 100644 index 00000000000..4eb26fa1a20 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt @@ -0,0 +1,106 @@ +/* + * Wire + * Copyright (C) 2023 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 + +import androidx.lifecycle.SavedStateHandle +import com.wire.android.config.CoroutineTestExtension +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class CompositeMessageViewModelTest { + + @Test + fun `given pending button, when button is clicked, then do Nothing`() = runTest { + // Arrange + val (arrangement, viewModel) = Arrangement().arrange() + val buttonId = "buttonId" + viewModel.pendingButtonId = buttonId + + // Act + viewModel.sendButtonActionMessage(buttonId) + advanceUntilIdle() + + // Assert + coVerify(exactly = 0) { + arrangement.sendButtonActionMessage(any(), any(), any()) + } + } + + @Test + fun `given button nto pending, when button is clicked, then mark pending and then remove it once done`() = runTest { + // Arrange + val (arrangement, viewModel) = Arrangement() + .withButtonActionMessage(SendButtonActionMessageUseCase.Result.Success) + .arrange() + + val buttonId = "buttonId" + + // Act + viewModel.sendButtonActionMessage(buttonId) + advanceUntilIdle() + assertNull(viewModel.pendingButtonId) + + // Assert + coVerify(exactly = 1) { + arrangement.sendButtonActionMessage(any(), any(), any()) + } + } + + private companion object { + const val CONVERSION_ID_STRING = "some-dummy-value@some.dummy.domain" + const val MESSAGE_ID = "message-id" + } + + private class Arrangement { + + @MockK + lateinit var sendButtonActionMessage: SendButtonActionMessageUseCase + val qualifiedIdMapper: QualifiedIdMapper = QualifiedIdMapperImpl(null) + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + init { + MockKAnnotations.init(this) + every { savedStateHandle.get(any()) } returns CONVERSION_ID_STRING + every { savedStateHandle.get(any()) } returns MESSAGE_ID + } + + private val viewModel = CompositeMessageViewModel(sendButtonActionMessage, qualifiedIdMapper, savedStateHandle) + + fun withButtonActionMessage( + result: SendButtonActionMessageUseCase.Result + ) = apply { + coEvery { sendButtonActionMessage(any(), any(), any()) } returns result + } + + fun arrange() = this to viewModel + } +} diff --git a/kalium b/kalium index 03057fa341b..67514c7ec13 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 03057fa341bd02f63c33309afc7074882eda3198 +Subproject commit 67514c7ec1394fbb477c9c3fb92600838f2642a5