diff --git a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt index 5e29cda3284..40464339875 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/MessageMapper.kt @@ -27,6 +27,7 @@ import com.wire.android.ui.home.conversations.model.MessageEditStatus import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.MessageFooter import com.wire.android.ui.home.conversations.model.MessageHeader +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.MessageSource import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime @@ -46,6 +47,7 @@ import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.User import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.type.UserTypeInfo import javax.inject.Inject class MessageMapper @Inject constructor( @@ -165,7 +167,12 @@ class MessageMapper @Inject constructor( }, clientId = (message as? Message.Sendable)?.senderClientId, accent = Accent.fromAccentId(sender?.accentId), - guestExpiresAt = sender?.expiresAt + guestExpiresAt = sender?.expiresAt, + senderId = when { + (sender as? OtherUser)?.botService != null -> MessageSenderId.Bot(sender.botService!!) + sender?.userType == UserTypeInfo.App -> MessageSenderId.App(sender.id) + else -> MessageSenderId.User(sender?.id?.toString()) + } ) private fun getMessageStatus(message: Message.Standalone): MessageStatus { 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 507195c12a8..67d76522657 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 @@ -92,6 +92,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.MediaGalleryS import com.ramcosta.composedestinations.generated.app.destinations.MessageDetailsScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ServiceDetailsScreenDestination import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination import com.ramcosta.composedestinations.result.NavResult.Canceled import com.ramcosta.composedestinations.result.NavResult.Value @@ -171,6 +172,7 @@ import com.wire.android.ui.home.conversations.messages.item.MessageContainerItem import com.wire.android.ui.home.conversations.messages.item.SwipeableMessageConfiguration import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.ExpirationStatus +import com.wire.android.ui.home.conversations.model.MessageSenderId 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 @@ -191,6 +193,7 @@ import com.wire.android.ui.legalhold.dialog.subject.LegalHoldSubjectMessageDialo import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.service.ServiceDetailsNavArgs import com.wire.android.util.DateAndTimeParsers import com.wire.android.util.normalizeLink import com.wire.android.util.openDownloadFolder @@ -206,6 +209,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.MessageAssetStatus import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.type.UserTypeInfo import com.wire.kalium.logic.data.user.type.isInternal import com.wire.kalium.logic.data.user.type.isTeamAdmin import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult @@ -497,16 +501,38 @@ fun ConversationScreen( conversationInfoViewState = conversationInfoViewModel.conversationInfoViewState, conversationMessagesViewState = conversationMessagesViewModel.conversationViewState, attachments = messageAttachmentsViewModel.attachments, - onOpenProfile = { + onOpenProfile = { senderId: MessageSenderId -> with(conversationInfoViewModel) { - val (mentionUserId: UserId, isSelfUser: Boolean) = mentionedUserData(it) - if (isSelfUser) { - navigator.navigate(NavigationCommand(SelfUserProfileScreenDestination)) - } else { - (conversationInfoViewState.conversationDetailsData as? ConversationDetailsData.Group)?.conversationId.let { - navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(mentionUserId, it))) + val route = when (senderId) { + is MessageSenderId.Bot -> ServiceDetailsScreenDestination( + null, + ServiceDetailsNavArgs.Id.BotServiceId(senderId.botService) + ) + + is MessageSenderId.App -> ServiceDetailsScreenDestination( + null, + ServiceDetailsNavArgs.Id.AppId(senderId.appId) + ) + + is MessageSenderId.User -> { + val (mentionUserId: UserId, isSelfUser: Boolean) = mentionedUserData(senderId.id.toString()) + if (isSelfUser) { + SelfUserProfileScreenDestination + } else { + (conversationInfoViewState.conversationDetailsData as? ConversationDetailsData.Group) + ?.conversationId?.let { conversationId -> + OtherUserProfileScreenDestination( + mentionUserId, + conversationId + ) + } + } } } + + route?.let { + navigator.navigate(NavigationCommand(it)) + } } }, onMessageDetailsClick = { messageId: String, isSelfMessage: Boolean -> @@ -601,17 +627,38 @@ fun ConversationScreen( onUpdateConversationReadDate = messageComposerViewModel::updateConversationReadDate, onDropDownClick = { with(conversationInfoViewModel) { - when (val data = conversationInfoViewState.conversationDetailsData) { - is ConversationDetailsData.OneOne -> - navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(data.otherUserId))) + val route = when (val data = conversationInfoViewState.conversationDetailsData) { + is ConversationDetailsData.OneOne -> { + val botService = data.botService + when { + botService != null -> + ServiceDetailsScreenDestination( + null, + ServiceDetailsNavArgs.Id.BotServiceId(botService) + ) + + data.userType == UserTypeInfo.App -> + ServiceDetailsScreenDestination( + null, + ServiceDetailsNavArgs.Id.AppId(data.otherUserId) + ) + + else -> OtherUserProfileScreenDestination(data.otherUserId) + } + } is ConversationDetailsData.Group -> - navigator.navigate(NavigationCommand(GroupConversationDetailsScreenDestination(conversationId))) + GroupConversationDetailsScreenDestination(conversationId) is ConversationDetailsData.None -> { /* do nothing */ + null } } + + route?.let { + navigator.navigate(NavigationCommand(it)) + } } }, onBackButtonClick = { @@ -915,7 +962,7 @@ private fun ConversationScreen( conversationMessagesViewState: ConversationMessagesViewState, attachments: List, bottomSheetVisible: Boolean, - onOpenProfile: (String) -> Unit, + onOpenProfile: (senderId: MessageSenderId) -> Unit, onMessageDetailsClick: (messageId: String, isSelfMessage: Boolean) -> Unit, onSendMessage: (MessageBundle) -> Unit, onPingOptionClicked: () -> Unit, @@ -1123,7 +1170,7 @@ private fun ConversationScreenContent( onImageFullScreenMode: (UIMessage.Regular, Boolean, String?) -> Unit, onReactionClicked: (String, String) -> Unit, onResetSessionClicked: (senderUserId: UserId, clientId: String?) -> Unit, - onOpenProfile: (String) -> Unit, + onOpenProfile: (senderId: MessageSenderId) -> Unit, onUpdateConversationReadDate: (String) -> Unit, onShowEditingOptions: (UIMessage.Regular) -> Unit, onSwipedToReply: (UIMessage.Regular) -> Unit, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index 43f49220cdd..4479d86a6fd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -149,6 +149,8 @@ class ConversationInfoViewModel @Inject constructor( connectionState = conversationDetails.otherUser.connectionStatus, isBlocked = conversationDetails.otherUser.connectionStatus == ConnectionState.BLOCKED, isDeleted = conversationDetails.otherUser.deleted, + botService = conversationDetails.otherUser.botService, + userType = conversationDetails.otherUser.userType ) else -> ConversationDetailsData.None(conversationDetails.conversation.protocol) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt index fc5182053ff..1c67671a567 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt @@ -22,9 +22,11 @@ import com.wire.android.model.ImageAsset import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.user.BotService import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserAvailabilityStatus import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.type.UserTypeInfo data class ConversationInfoViewState( val conversationId: QualifiedID, @@ -52,7 +54,9 @@ sealed class ConversationDetailsData(open val conversationProtocol: Conversation val otherUserName: String?, val connectionState: ConnectionState, val isBlocked: Boolean, - val isDeleted: Boolean + val isDeleted: Boolean, + val botService: BotService?, + val userType: UserTypeInfo ) : ConversationDetailsData(conversationProtocol) data class Group( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageClickActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageClickActions.kt index 8d749621874..cdd8dfd73db 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageClickActions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageClickActions.kt @@ -17,6 +17,7 @@ */ package com.wire.android.ui.home.conversations.messages.item +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId @@ -24,7 +25,7 @@ import com.wire.kalium.logic.data.user.UserId sealed class MessageClickActions { open val onFullMessageClicked: ((messageId: String) -> Unit)? = null open val onFullMessageLongClicked: ((UIMessage.Regular) -> Unit)? = null - open val onProfileClicked: (String) -> Unit = {} + open val onProfileClicked: (senderId: MessageSenderId) -> Unit = {} open val onReactionClicked: (String, String) -> Unit = { _, _ -> } open val onAssetClicked: (String) -> Unit = {} open val onImageClicked: (UIMessage.Regular, Boolean, String?) -> Unit = { _, _, _ -> } @@ -41,7 +42,7 @@ sealed class MessageClickActions { data class Content( override val onFullMessageLongClicked: ((UIMessage.Regular) -> Unit)? = null, - override val onProfileClicked: (String) -> Unit = {}, + override val onProfileClicked: (senderId: MessageSenderId) -> Unit = {}, override val onReactionClicked: (String, String) -> Unit = { _, _ -> }, override val onAssetClicked: (String) -> Unit = {}, override val onImageClicked: (UIMessage.Regular, Boolean, String?) -> Unit = { _, _, _ -> }, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt index 5ecf1daebb0..108a35d1fcd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt @@ -45,6 +45,7 @@ import com.wire.android.ui.home.conversations.messages.QuotedUnavailable 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.MessageImage +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.MessageSource import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent @@ -70,7 +71,7 @@ internal fun UIMessage.Regular.MessageContentAndStatus( messageStyle: MessageStyle, onAssetClicked: (String) -> Unit, onImageClicked: (UIMessage.Regular, Boolean, String?) -> Unit, - onProfileClicked: (String) -> Unit, + onProfileClicked: (senderId: MessageSenderId) -> Unit, onLinkClicked: (String) -> Unit, onReplyClicked: (UIMessage.Regular) -> Unit, shouldDisplayMessageStatus: Boolean, @@ -163,7 +164,7 @@ private fun MessageContent( onAssetClick: Clickable, onImageClick: Clickable, onMultipartImageClick: (String) -> Unit, - onOpenProfile: (String) -> Unit, + onOpenProfile: (senderId: MessageSenderId) -> Unit, onLinkClick: (String) -> Unit, onReplyClick: Clickable, accent: Accent, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt index 8fa2d6bde91..d089f51b625 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItemLeading.kt @@ -26,6 +26,7 @@ import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.avatar.UserProfileAvatar import com.wire.android.ui.common.avatar.UserProfileAvatarType.WithIndicators import com.wire.android.ui.home.conversations.model.MessageHeader +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.common.R as commonR @Composable @@ -33,7 +34,7 @@ fun RegularMessageItemLeading( header: MessageHeader, showAuthor: Boolean, userAvatarData: UserAvatarData, - onOpenProfile: (String) -> Unit + onOpenProfile: (MessageSenderId) -> Unit ) { val isProfileRedirectEnabled = header.userId != null && !(header.isSenderDeleted || header.isSenderUnavailable) @@ -44,7 +45,7 @@ fun RegularMessageItemLeading( enabled = isProfileRedirectEnabled, onClickDescription = openProfileDescription ) { - onOpenProfile(header.userId!!.toString()) + onOpenProfile(header.senderId) } } val avatarContentDescription = listOfNotNull( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt index 69ae8b8af91..09dcf9f91d8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt @@ -28,6 +28,7 @@ import com.wire.android.ui.home.conversations.model.MessageEditStatus import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.MessageFooter import com.wire.android.ui.home.conversations.model.MessageHeader +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.MessageSource import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime @@ -87,7 +88,8 @@ val mockHeader = MessageHeader( messageId = "", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ) fun mockHeaderWithExpiration(expirable: ExpirationStatus.Expirable, isDeleted: Boolean = false) = mockHeader.copy( @@ -337,7 +339,8 @@ fun mockAssetMessage(assetId: String = "asset1", messageId: String = "msg1") = U messageId = messageId, connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = UIMessageContent.AssetMessage( assetName = "This is some test asset message that has a not so long title", @@ -371,7 +374,8 @@ fun mockAssetAudioMessage( messageId = messageId, connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = UIMessageContent.AudioAssetMessage( assetName = "Audio message", @@ -434,7 +438,8 @@ fun mockedImageUIMessage( messageId = messageId, connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), source: MessageSource = MessageSource.Self ) = UIMessage.Regular( @@ -482,7 +487,8 @@ fun mockedMultipartMessage( messageId = messageId, connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), source: MessageSource = MessageSource.Self, content: UIMessageContent.Regular = UIMessageContent.Multipart( @@ -521,7 +527,8 @@ fun getMockedMessages(): List = listOf( messageId = "1", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = UIMessageContent.TextMessage( messageBody = MessageBody( @@ -552,7 +559,8 @@ fun getMockedMessages(): List = listOf( messageId = "2", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = mockedImg(), source = MessageSource.Self, @@ -574,7 +582,8 @@ fun getMockedMessages(): List = listOf( messageId = "3", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = mockedImg(), source = MessageSource.Self, @@ -596,7 +605,8 @@ fun getMockedMessages(): List = listOf( messageId = "4", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = mockedImg(), source = MessageSource.Self, @@ -618,7 +628,8 @@ fun getMockedMessages(): List = listOf( messageId = "5", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = UIMessageContent.TextMessage( messageBody = MessageBody( @@ -649,7 +660,8 @@ fun getMockedMessages(): List = listOf( messageId = "6", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = mockedImg(), source = MessageSource.Self, @@ -671,7 +683,8 @@ fun getMockedMessages(): List = listOf( messageId = "7", connectionState = ConnectionState.ACCEPTED, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ), messageContent = UIMessageContent.TextMessage( messageBody = MessageBody( 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 d53987364d2..c62a4c9513f 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 @@ -99,7 +99,7 @@ internal fun MessageBody( messageBody: MessageBody?, isAvailable: Boolean, accent: Accent, - onOpenProfile: (String) -> Unit, + onOpenProfile: (senderId: MessageSenderId) -> Unit, buttonList: PersistentList?, onLinkClick: (String) -> Unit, searchQuery: String = "", 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 03628259c33..08d70919ef3 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 @@ -43,6 +43,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageAttachment import com.wire.kalium.logic.data.user.AssetId +import com.wire.kalium.logic.data.user.BotService import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString @@ -163,9 +164,16 @@ data class MessageHeader( val isSenderUnavailable: Boolean, val clientId: ClientId? = null, val accent: Accent = Accent.Unknown, - val guestExpiresAt: Instant? = null + val guestExpiresAt: Instant? = null, + val senderId: MessageSenderId ) +sealed interface MessageSenderId { + data class User(val id: String?) : MessageSenderId + data class App(val appId: UserId) : MessageSenderId + data class Bot(val botService: BotService) : MessageSenderId +} + @Stable @Serializable data class MessageFooter( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt index 369ddcbc6a6..cbe24b11867 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt @@ -37,6 +37,7 @@ import com.wire.android.ui.home.newconversation.channelhistory.ChannelHistoryTyp import com.wire.android.ui.home.newconversation.common.CreateGroupState import com.wire.android.ui.home.newconversation.groupOptions.GroupOptionState import com.wire.android.ui.home.newconversation.model.Contact +import com.wire.android.util.AppsUtil import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.CreateConversationParam import com.wire.kalium.logic.data.user.UserId @@ -101,6 +102,10 @@ class NewConversationViewModel @Inject constructor( .collectLatest { isAppsAllowedResult -> groupOptionsState = groupOptionsState.copy( isTeamAllowedToUseApps = isAppsAllowedResult, + shouldShowNewAppsUi = AppsUtil.isAppsAllowed( + appsAllowedResult = isAppsAllowedResult, + conversationProtocol = null + ), isAllowAppsEnabled = isAppsAllowedResult is AppsAllowedResult.Enabled ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionState.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionState.kt index 5e331c394a4..c9b72f1f37b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/groupOptions/GroupOptionState.kt @@ -29,5 +29,6 @@ data class GroupOptionState( val showAllowGuestsDialog: Boolean = false, // feature flag for allowing apps usage for the team val isTeamAllowedToUseApps: AppsAllowedResult = AppsAllowedResult.Disabled, + val shouldShowNewAppsUi: Boolean = false, val isWireCellsEnabled: Boolean? = null, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt index 769ced1edc2..4d0e5db38d0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/search/NewConversationSearchPeopleScreen.kt @@ -29,10 +29,14 @@ import com.wire.android.navigation.style.PopUpNavigationAnimation import com.ramcosta.composedestinations.generated.app.navgraphs.PersonalToTeamMigrationGraph import com.ramcosta.composedestinations.generated.app.destinations.NewGroupConversationSearchPeopleScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ServiceDetailsScreenDestination import com.wire.android.ui.home.conversations.search.SearchPeopleScreenType import com.wire.android.ui.home.conversations.search.SearchUsersAndAppsScreen import com.wire.android.ui.home.newconversation.NewConversationViewModel +import com.wire.android.ui.userprofile.service.ServiceDetailsNavArgs import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.user.BotService +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult @WireNewConversationDestination( @@ -69,8 +73,25 @@ fun NewConversationSearchPeopleScreen( isAppsTabVisible = (newConversationViewModel.groupOptionsState.isTeamAllowedToUseApps is AppsAllowedResult.Enabled), conversationProtocol = null, onAppClicked = { contact -> - OtherUserProfileScreenDestination(QualifiedID(contact.id, contact.domain)) - .let { navigator.navigate(NavigationCommand(it)) } + val serviceDetailsNavArgsId: ServiceDetailsNavArgs.Id = + if (newConversationViewModel.groupOptionsState.shouldShowNewAppsUi) { + ServiceDetailsNavArgs.Id.AppId( + UserId(contact.id, contact.domain) + ) + } else { + ServiceDetailsNavArgs.Id.BotServiceId( + BotService(id = contact.id, provider = contact.domain) + ) + } + + navigator.navigate( + NavigationCommand( + ServiceDetailsScreenDestination( + null, + serviceDetailsNavArgsId + ) + ) + ) } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownText.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownText.kt index 6b83572b581..d31c6d73af4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownText.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/MarkdownText.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import com.wire.android.ui.common.ClickableText +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.markdown.MarkdownConstants.TAG_MENTION import com.wire.android.ui.markdown.MarkdownConstants.TAG_URL @@ -47,7 +48,7 @@ fun MarkdownText( clickable: Boolean = true, onClickLink: ((linkText: String) -> Unit)? = null, onLongClick: (() -> Unit)? = null, - onOpenProfile: ((String) -> Unit)? = null + onOpenProfile: ((senderId: MessageSenderId) -> Unit)? = null ) { if (clickable) { @@ -75,7 +76,7 @@ fun MarkdownText( start = offset, end = offset ).firstOrNull()?.let { result -> - onOpenProfile?.invoke(result.item) + onOpenProfile?.invoke(MessageSenderId.User(result.item)) } }, onLongClick = onLongClick diff --git a/app/src/main/kotlin/com/wire/android/ui/markdown/NodeData.kt b/app/src/main/kotlin/com/wire/android/ui/markdown/NodeData.kt index 803639920c8..9376a44fdf7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/markdown/NodeData.kt +++ b/app/src/main/kotlin/com/wire/android/ui/markdown/NodeData.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import com.wire.android.ui.home.conversations.messages.item.MessageStyle +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.theme.Accent import com.wire.android.ui.theme.WireColorScheme import com.wire.android.ui.theme.WireTypography @@ -45,7 +46,7 @@ data class MessageColors(val highlighted: Color) data class NodeActions( val onLongClick: (() -> Unit)? = null, - val onOpenProfile: (String) -> Unit, + val onOpenProfile: (senderId: MessageSenderId) -> Unit, val onLinkClick: (String) -> Unit ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsInfoMessageType.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsInfoMessageType.kt index 40d0ac9605b..3c81b8c611d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsInfoMessageType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsInfoMessageType.kt @@ -30,4 +30,9 @@ sealed class ServiceDetailsInfoMessageType(override val uiText: UIText) : SnackB // Add Service object SuccessAddService : ServiceDetailsInfoMessageType(UIText.StringResource(R.string.service_add_success)) object ErrorAddService : ServiceDetailsInfoMessageType(UIText.StringResource(R.string.service_add_error)) + + // Start or Open Conversation + object ErrorStartOrOpenConversation : ServiceDetailsInfoMessageType( + UIText.StringResource(R.string.service_conversation_creation_or_retrieval_error) + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgs.kt index f1ac415efa8..49fb12aa70d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgs.kt @@ -34,15 +34,22 @@ data class ServiceDetailsNavArgs( ) { sealed interface Id { val serviceId: ServiceId + val userId: UserId data class BotServiceId(val botService: BotService) : Id { override val serviceId: ServiceId get() = ServiceId(botService.id, botService.provider) + + override val userId: UserId + get() = UserId(botService.id, botService.provider) } data class AppId(val appId: UserId) : Id { override val serviceId: ServiceId get() = ServiceId(appId.value, appId.domain) + + override val userId: UserId + get() = appId } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt index fa7be0e4c33..33b95c15366 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.wire.android.R import com.wire.android.model.ClickBlockParams import com.wire.android.model.NameBasedAvatar @@ -47,6 +48,8 @@ import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.avatar.UserProfileAvatar import com.wire.android.ui.common.avatar.UserProfileAvatarType import com.wire.android.model.Clickable +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.UserBadge @@ -56,6 +59,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions @@ -80,10 +84,26 @@ fun ServiceDetailsScreen( } } + LaunchedEffect(Unit) { + viewModel.openConversationEvent.collect { conversationId -> + conversationId?.let { + navigator.navigate( + NavigationCommand( + ConversationScreenDestination( + navArgs = ConversationNavArgs(conversationId = conversationId) + ), + BackStackMode.UPDATE_EXISTED + ) + ) + } + } + } + ServiceDetailsContent( navigateBack = navigator::navigateBack, - addService = viewModel::addService, - removeService = viewModel::removeService, + onAddService = viewModel::onAddService, + onRemoveService = viewModel::onRemoveService, + onOpenConversation = viewModel::onOpenConversation, serviceDetailsState = viewModel.serviceDetailsState ) } @@ -91,8 +111,9 @@ fun ServiceDetailsScreen( @Composable private fun ServiceDetailsContent( navigateBack: () -> Unit, - addService: () -> Unit, - removeService: () -> Unit, + onAddService: () -> Unit, + onRemoveService: () -> Unit, + onOpenConversation: () -> Unit, serviceDetailsState: ServiceDetailsState ) { WireScaffold( @@ -112,10 +133,11 @@ private fun ServiceDetailsContent( } }, bottomBar = { - ServiceDetailsAddOrRemoveButton( - buttonState = serviceDetailsState.buttonState, - addService = addService, - removeService = removeService + ServiceDetailsButtons( + serviceDetailsState = serviceDetailsState, + onAddService = onAddService, + onRemoveService = onRemoveService, + onOpenConversation = onOpenConversation ) } ) @@ -223,8 +245,8 @@ private fun ServiceDetailsDescription(serviceDetails: ServiceDetails) { @Composable private fun ServiceDetailsAddOrRemoveButton( buttonState: ServiceDetailsButtonState, - addService: () -> Unit, - removeService: () -> Unit + onAddService: () -> Unit, + onRemoveService: () -> Unit ) { val (shouldShow: Boolean, textString: String?) = when (buttonState) { ServiceDetailsButtonState.HIDDEN -> Pair(false, null) @@ -243,7 +265,7 @@ private fun ServiceDetailsAddOrRemoveButton( ) { WirePrimaryButton( text = textString, - onClick = if (buttonState == ServiceDetailsButtonState.ADD) addService else removeService, + onClick = if (buttonState == ServiceDetailsButtonState.ADD) onAddService else onRemoveService, clickBlockParams = ClickBlockParams(blockWhenSyncing = true, blockWhenConnecting = true), modifier = Modifier .weight(1f) @@ -254,8 +276,73 @@ private fun ServiceDetailsAddOrRemoveButton( } } +@Composable +private fun ServiceDetailsStartOrOpenConversation( + isDataLoading: Boolean, + isConversationStarted: Boolean, + onOpenConversation: () -> Unit +) { + if (!isDataLoading) { + Surface( + color = MaterialTheme.wireColorScheme.background, + shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation + ) { + HorizontalDivider(color = colorsScheme().outline) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + WirePrimaryButton( + text = stringResource( + id = if (isConversationStarted) { + R.string.label_open_conversation + } else { + R.string.label_start_conversation + } + ), + onClick = onOpenConversation, + clickBlockParams = ClickBlockParams( + blockWhenSyncing = true, + blockWhenConnecting = true + ), + modifier = Modifier + .weight(1f) + .padding(dimensions().spacing16x) + ) + } + } + } +} + +@Composable +private fun ServiceDetailsButtons( + serviceDetailsState: ServiceDetailsState, + onAddService: () -> Unit, + onRemoveService: () -> Unit, + onOpenConversation: () -> Unit +) { + when { + serviceDetailsState.conversationId != null -> ServiceDetailsAddOrRemoveButton( + buttonState = serviceDetailsState.buttonState, + onAddService = onAddService, + onRemoveService = onRemoveService + ) + serviceDetailsState.isAppsEnabled -> ServiceDetailsStartOrOpenConversation( + isDataLoading = serviceDetailsState.isDataLoading, + isConversationStarted = serviceDetailsState.isConversationStarted, + onOpenConversation = onOpenConversation + ) + } +} + @Preview @Composable fun PreviewServiceDetailsScreen() { - ServiceDetailsContent({}, {}, {}, ServiceDetailsState()) + ServiceDetailsContent( + navigateBack = {}, + onAddService = {}, + onRemoveService = {}, + onOpenConversation = {}, + serviceDetailsState = ServiceDetailsState() + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsState.kt index 2cb19afa160..975348162c5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsState.kt @@ -31,8 +31,10 @@ data class ServiceDetailsState( val serviceAvatarAsset: ImageAsset.UserAvatarAsset? = null, val isDataLoading: Boolean = false, val isAvatarLoading: Boolean = false, + val isConversationStarted: Boolean = false, val buttonState: ServiceDetailsButtonState = ServiceDetailsButtonState.HIDDEN, - val serviceMemberId: QualifiedID? = null + val serviceMemberId: QualifiedID? = null, + val isAppsEnabled: Boolean = false ) data class ServiceDetailsGroupState( diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt index 00940bb6f9a..d236cf408bc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt @@ -27,10 +27,12 @@ import com.wire.android.di.CurrentAccount import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase import com.ramcosta.composedestinations.generated.app.navArgs +import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.AppsUtil import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.service.ServiceDetails import com.wire.kalium.logic.data.service.ServiceId @@ -40,6 +42,9 @@ import com.wire.kalium.logic.feature.app.ObserveIsAppMemberResult import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase +import com.wire.kalium.logic.feature.conversation.CreateConversationResult +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase @@ -75,28 +80,28 @@ class ServiceDetailsViewModel @Inject constructor( private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val addMemberToConversation: AddMemberToConversationUseCase, + private val isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase, + private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { private val serviceDetailsNavArgs: ServiceDetailsNavArgs = savedStateHandle.navArgs() private val serviceId: ServiceId = serviceDetailsNavArgs.id.serviceId + private val userId: UserId = serviceDetailsNavArgs.id.userId private val conversationId: QualifiedID? = serviceDetailsNavArgs.conversationId var serviceDetailsState by mutableStateOf(ServiceDetailsState()) - var isAppsEnabled by mutableStateOf(false) private val _infoMessage = MutableSharedFlow() val infoMessage = _infoMessage.asSharedFlow() + private val _openConversationEvent = MutableSharedFlow() + val openConversationEvent = _openConversationEvent.asSharedFlow() + init { viewModelScope.launch { - serviceDetailsState = serviceDetailsState.copy( - serviceId = serviceId, - conversationId = conversationId, - isDataLoading = true, - isAvatarLoading = true - ) + getIfConversationExist() val appsAllowedResult = observeIsAppsAllowedForUsage().firstOrNull() @@ -107,26 +112,37 @@ class ServiceDetailsViewModel @Inject constructor( .firstOrNull() } - isAppsEnabled = AppsUtil.isAppsAllowed( + val isAppsEnabled = AppsUtil.isAppsAllowed( appsAllowedResult = appsAllowedResult, conversationProtocol = conversationProtocolInfo ) - when { - isAppsEnabled && serviceDetailsNavArgs.id is ServiceDetailsNavArgs.Id.AppId -> { - getAppDetailsAndUpdateViewState(serviceDetailsNavArgs.id.appId) - ?.let { observeIsAppConversationMember(serviceDetailsNavArgs.id.appId) } + serviceDetailsState = serviceDetailsState.copy( + serviceId = serviceId, + conversationId = conversationId, + isDataLoading = true, + isAvatarLoading = true, + isAppsEnabled = isAppsEnabled + ) + + when (val id = serviceDetailsNavArgs.id) { + is ServiceDetailsNavArgs.Id.AppId -> { + val details = getAppDetailsAndUpdateViewState(id.appId) + if (details != null && isAppsEnabled) { + observeIsAppConversationMember(id.appId) + } } - !isAppsEnabled && serviceDetailsNavArgs.id is ServiceDetailsNavArgs.Id.BotServiceId -> { - getServiceDetailsAndUpdateViewState() - ?.let { observeIsServiceConversationMember() } + + is ServiceDetailsNavArgs.Id.BotServiceId -> { + getServiceDetailsAndUpdateViewState()?.let { + observeIsServiceConversationMember() + } } - else -> serviceNotFound() } } } - fun addService() { + fun onAddService() { viewModelScope.launch { val responseMessage = when (val id = serviceDetailsNavArgs.id) { is ServiceDetailsNavArgs.Id.AppId -> { @@ -159,7 +175,7 @@ class ServiceDetailsViewModel @Inject constructor( } } - fun removeService() { + fun onRemoveService() { viewModelScope.launch { serviceDetailsState.serviceMemberId?.let { serviceMemberId -> val response = withContext(dispatchers.io()) { @@ -179,6 +195,22 @@ class ServiceDetailsViewModel @Inject constructor( } } + fun onOpenConversation() { + viewModelScope.launch { + val result = withContext(dispatchers.io()) { + getOrCreateOneToOneConversation(userId) + } + + when (result) { + is CreateConversationResult.Failure -> { + appLogger.d("Couldn't retrieve or create the conversation") + _infoMessage.emit(ServiceDetailsInfoMessageType.ErrorStartOrOpenConversation.uiText) + } + is CreateConversationResult.Success -> _openConversationEvent.emit(result.conversation.id) + } + } + } + private suspend fun getServiceDetailsAndUpdateViewState(): ServiceDetails? = getServiceById(serviceId = serviceId).also { service -> if (service != null) { @@ -265,6 +297,17 @@ class ServiceDetailsViewModel @Inject constructor( } } + private fun getIfConversationExist() { + viewModelScope.launch { + if (conversationId == null) { + val isOneToOneConversationCreated = isOneToOneConversationCreated(userId) + serviceDetailsState = serviceDetailsState.copy( + isConversationStarted = isOneToOneConversationCreated + ) + } + } + } + private fun serviceNotFound() { serviceDetailsState = serviceDetailsState.copy( serviceDetails = null, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4796c023380..63c83002d47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1240,6 +1240,7 @@ Unable to remove app from conversation App added to conversation Unable to add app to conversation + Couldn\'t retrieve or create the conversation No information available Try again or reach out to your team admin Read receipts diff --git a/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt b/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt index cac03c6a25a..dfd6b8c55d9 100644 --- a/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt +++ b/app/src/test/kotlin/com/wire/android/framework/TestMessage.kt @@ -21,6 +21,7 @@ package com.wire.android.framework import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.MessageHeader +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime import com.wire.android.ui.home.conversationslist.model.Membership @@ -154,7 +155,8 @@ object TestMessage { messageId = "messageID", connectionState = null, isSenderDeleted = false, - isSenderUnavailable = false + isSenderUnavailable = false, + senderId = MessageSenderId.User(null) ) val MISSED_CALL_MESSAGE = Message.System( id = "messageID", diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index d598fedad15..834bc59b8be 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -217,6 +217,8 @@ internal fun withMockConversationDetailsOneOnOne( every { isUnavailableUser } returns unavailable every { deleted } returns false every { accentId } returns 0 + every { botService } returns null + every { userType } returns UserTypeInfo.Regular(UserType.NONE) }, userType = UserTypeInfo.Regular(UserType.INTERNAL), ) 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 816f6e469a8..3501bd88fe0 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 @@ -26,6 +26,7 @@ import com.wire.android.ui.home.conversations.model.MessageBody import com.wire.android.ui.home.conversations.model.MessageFlowStatus import com.wire.android.ui.home.conversations.model.MessageFooter import com.wire.android.ui.home.conversations.model.MessageHeader +import com.wire.android.ui.home.conversations.model.MessageSenderId import com.wire.android.ui.home.conversations.model.MessageSource import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime @@ -246,6 +247,7 @@ class GetQuoteMessageForConversationUseCaseTest { connectionState = null, isSenderDeleted = false, isSenderUnavailable = false, + senderId = MessageSenderId.User(USER_ID.toString()) ), source = MessageSource.OtherUser, userAvatarData = UserAvatarData(), diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt index 90bb34f4ea1..168eecef379 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt @@ -44,6 +44,9 @@ import com.wire.kalium.logic.feature.app.ObserveIsAppMemberResult import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase +import com.wire.kalium.logic.feature.conversation.CreateConversationResult +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.featureConfig.AppsAllowedProtocol @@ -213,7 +216,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.removeService() + viewModel.onRemoveService() // then coVerify(exactly = 1) { @@ -240,7 +243,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.removeService() + viewModel.onRemoveService() // then coVerify(exactly = 1) { @@ -267,7 +270,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.addService() + viewModel.onAddService() // then coVerify(exactly = 1) { @@ -294,7 +297,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.addService() + viewModel.onAddService() // then coVerify(exactly = 1) { @@ -372,6 +375,8 @@ class ServiceDetailsViewModelTest { // then assertEquals(APP_SERVICE_DETAILS, viewModel.serviceDetailsState.serviceDetails) assertEquals(null, viewModel.serviceDetailsState.serviceMemberId) + assertEquals(true, viewModel.serviceDetailsState.isAppsEnabled) + assertEquals(false, viewModel.serviceDetailsState.isConversationStarted) assertEquals(ServiceDetailsButtonState.HIDDEN, viewModel.serviceDetailsState.buttonState) } @@ -440,7 +445,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.removeService() + viewModel.onRemoveService() // then coVerify(exactly = 1) { @@ -470,7 +475,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.removeService() + viewModel.onRemoveService() // then coVerify(exactly = 1) { @@ -500,7 +505,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.addService() + viewModel.onAddService() // then coVerify(exactly = 1) { @@ -530,7 +535,7 @@ class ServiceDetailsViewModelTest { // when viewModel.infoMessage.test { - viewModel.addService() + viewModel.onAddService() // then coVerify(exactly = 1) { @@ -544,6 +549,85 @@ class ServiceDetailsViewModelTest { } } + @Test + fun `given user opens service details screen from create conversation flow, when one to one conversation already exists, then conversation started state is shown`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withServiceApp( + service = APP_ID, + conversationId = null + ) + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.MIXED(SupportedProtocol.MLS))) + .withAppDetails(APP_ID, APP_SERVICE_DETAILS) + .withOneToOneConversationCreated(isCreated = true) + .arrange() + + // when + // view model is initialized + + // then + assertEquals(true, viewModel.serviceDetailsState.isConversationStarted) + } + + @Test + fun `given user opens service details screen, when opening conversation succeeds, then conversation event is emitted`() = + runTest { + // given + val (arrangement, viewModel) = Arrangement() + .withServiceApp( + service = APP_ID, + conversationId = null + ) + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.MIXED(SupportedProtocol.MLS))) + .withAppDetails(APP_ID, APP_SERVICE_DETAILS) + .withGetOrCreateOneToOneConversation( + CreateConversationResult.Success( + conversation = TestConversation.ONE_ON_ONE.copy(id = CONVERSATION_ID) + ) + ) + .arrange() + + // when + viewModel.openConversationEvent.test { + viewModel.onOpenConversation() + + // then + coVerify(exactly = 1) { + arrangement.getOrCreateOneToOneConversation(APP_ID) + } + assertEquals(CONVERSATION_ID, awaitItem()) + } + } + + @Test + fun `given user opens service details screen, when opening conversation fails, then error message is emitted`() = + runTest { + // given + val (arrangement, viewModel) = Arrangement() + .withServiceApp( + service = APP_ID, + conversationId = null + ) + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.MIXED(SupportedProtocol.MLS))) + .withAppDetails(APP_ID, APP_SERVICE_DETAILS) + .withGetOrCreateOneToOneConversation( + CreateConversationResult.Failure(CoreFailure.Unknown(null)) + ) + .arrange() + + // when + viewModel.infoMessage.test { + viewModel.onOpenConversation() + + // then + coVerify(exactly = 1) { + arrangement.getOrCreateOneToOneConversation(APP_ID) + } + assertEquals(ServiceDetailsInfoMessageType.ErrorStartOrOpenConversation.uiText, awaitItem()) + } + } + companion object { const val serviceId = "serviceId" const val providerId = "providerId" @@ -621,6 +705,12 @@ class ServiceDetailsViewModelTest { @MockK lateinit var addMemberToConversation: AddMemberToConversationUseCase + @MockK + lateinit var isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase + + @MockK + lateinit var getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase + @MockK lateinit var savedStateHandle: SavedStateHandle @@ -640,6 +730,8 @@ class ServiceDetailsViewModelTest { removeMemberFromConversation, addServiceToConversation, addMemberToConversation, + isOneToOneConversationCreated, + getOrCreateOneToOneConversation, savedStateHandle ) } @@ -656,6 +748,9 @@ class ServiceDetailsViewModelTest { coEvery { observeConversationDetails(any()) } returns flowOf(ObserveConversationDetailsUseCase.Result.Success(CONVERSATION_GROUP)) + coEvery { + isOneToOneConversationCreated(any()) + } returns false } fun withServiceBot(service: BotService, conversationId: ConversationId? = CONVERSATION_ID) = apply { @@ -722,6 +817,14 @@ class ServiceDetailsViewModelTest { coEvery { addServiceToConversation(any(), any()) } returns result } + fun withOneToOneConversationCreated(isCreated: Boolean) = apply { + coEvery { isOneToOneConversationCreated(any()) } returns isCreated + } + + fun withGetOrCreateOneToOneConversation(result: CreateConversationResult) = apply { + coEvery { getOrCreateOneToOneConversation(any()) } returns result + } + fun arrange() = this to viewModel } }