diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/InvalidLinkDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/InvalidLinkDialog.kt index 2979dc21e66..bbd1aa3ccf5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/InvalidLinkDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/InvalidLinkDialog.kt @@ -31,7 +31,7 @@ fun InvalidLinkDialog(dialogState: InvalidLinkDialogState, hideDialog: () -> Uni if (dialogState is InvalidLinkDialogState.Visible) { WireDialog( title = stringResource(R.string.label_invalid_link_title), - text = stringResource(R.string.invalid_link), + text = stringResource(R.string.invalid_link_dialog_body), buttonsHorizontalAlignment = false, onDismiss = hideDialog, optionButton1Properties = WireDialogButtonProperties( diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/VisitLinkDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/VisitLinkDialog.kt new file mode 100644 index 00000000000..e598272009e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/VisitLinkDialog.kt @@ -0,0 +1,49 @@ +/* + * 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.common.dialogs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.home.conversations.VisitLinkDialogState + +@Composable +fun VisitLinkDialog(dialogState: VisitLinkDialogState, hideDialog: () -> Unit) { + if (dialogState is VisitLinkDialogState.Visible) { + WireDialog( + title = stringResource(R.string.label_visit_link_title), + text = stringResource(R.string.visit_link_dialog_body, dialogState.link), + buttonsHorizontalAlignment = false, + onDismiss = hideDialog, + optionButton1Properties = WireDialogButtonProperties( + text = stringResource(R.string.label_open), + type = WireDialogButtonType.Primary, + onClick = dialogState.openLink + ), + optionButton2Properties = WireDialogButtonProperties( + text = stringResource(R.string.label_cancel), + type = WireDialogButtonType.Primary, + onClick = hideDialog + ) + ) + } +} 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 ddf7b1f8a75..cb19aef789a 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 @@ -21,11 +21,7 @@ package com.wire.android.ui.home.conversations import android.net.Uri -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Scaffold @@ -65,18 +61,13 @@ import com.wire.android.navigation.Navigator import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.dialogs.InvalidLinkDialog +import com.wire.android.ui.common.dialogs.VisitLinkDialog import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableDialog import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dialogs.calling.OngoingActiveCallDialog import com.wire.android.ui.common.error.CoreFailureErrorDialog import com.wire.android.ui.common.snackbar.SwipeDismissSnackbarHost -import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination -import com.wire.android.ui.destinations.InitiatingCallScreenDestination -import com.wire.android.ui.destinations.MediaGalleryScreenDestination -import com.wire.android.ui.destinations.MessageDetailsScreenDestination -import com.wire.android.ui.destinations.OngoingCallScreenDestination -import com.wire.android.ui.destinations.OtherUserProfileScreenDestination -import com.wire.android.ui.destinations.SelfUserProfileScreenDestination +import com.wire.android.ui.destinations.* import com.wire.android.ui.home.conversations.ConversationSnackbarMessages.OnFileDownloaded import com.wire.android.ui.home.conversations.banner.ConversationBanner import com.wire.android.ui.home.conversations.banner.ConversationBannerViewModel @@ -100,6 +91,7 @@ import com.wire.android.ui.home.messagecomposer.MessageComposer import com.wire.android.ui.home.messagecomposer.state.MessageBundle import com.wire.android.ui.home.messagecomposer.state.MessageComposerStateHolder import com.wire.android.ui.home.messagecomposer.state.rememberMessageComposerStateHolder +import com.wire.android.util.normalizeLink import com.wire.android.util.permission.CallingAudioRequestFlow import com.wire.android.util.permission.rememberCallingRecordAudioBluetoothRequestFlow import com.wire.android.util.ui.UIText @@ -296,10 +288,15 @@ fun ConversationScreen( messageComposerStateHolder = messageComposerStateHolder, onLinkClick = { link -> with(messageComposerViewModel) { - if (isLinkValid(link)) { - uriHandler.openUri(link) - } else { - invalidLinkDialogState = InvalidLinkDialogState.Visible + val normalizedLink = normalizeLink(link) + visitLinkDialogState = VisitLinkDialogState.Visible(normalizedLink) { + try { + uriHandler.openUri(normalizedLink) + visitLinkDialogState = VisitLinkDialogState.Hidden + } catch (_: Exception) { + visitLinkDialogState = VisitLinkDialogState.Hidden + invalidLinkDialogState = InvalidLinkDialogState.Visible + } } } }, @@ -318,6 +315,11 @@ fun ConversationScreen( dialogState = messageComposerViewModel.assetTooLargeDialogState, hideDialog = messageComposerViewModel::hideAssetTooLargeError ) + VisitLinkDialog( + dialogState = messageComposerViewModel.visitLinkDialogState, + hideDialog = messageComposerViewModel::hideVisitLinkDialog + ) + InvalidLinkDialog( dialogState = messageComposerViewModel.invalidLinkDialogState, hideDialog = messageComposerViewModel::hideInvalidLinkError diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt index 0e6f1759c46..bc5a7f79571 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewModel.kt @@ -55,17 +55,8 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase -import com.wire.kalium.logic.feature.conversation.InteractionAvailability -import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult -import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSecurityClassificationLabelUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase -import com.wire.kalium.logic.feature.message.DeleteMessageUseCase -import com.wire.kalium.logic.feature.message.RetryFailedMessageUseCase -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.conversation.* +import com.wire.kalium.logic.feature.message.* import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase @@ -150,6 +141,10 @@ class MessageComposerViewModel @Inject constructor( AssetTooLargeDialogState.Hidden ) + var visitLinkDialogState: VisitLinkDialogState by mutableStateOf( + VisitLinkDialogState.Hidden + ) + var invalidLinkDialogState: InvalidLinkDialogState by mutableStateOf( InvalidLinkDialogState.Hidden ) @@ -438,10 +433,13 @@ class MessageComposerViewModel @Inject constructor( assetTooLargeDialogState = AssetTooLargeDialogState.Hidden } + fun hideVisitLinkDialog() { + visitLinkDialogState = VisitLinkDialogState.Hidden + } + fun hideInvalidLinkError() { invalidLinkDialogState = InvalidLinkDialogState.Hidden } - companion object { private const val sizeOf1MB = 1024 * 1024 } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt index 7b6415f89d1..881e9dba4eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageComposerViewState.kt @@ -40,7 +40,13 @@ sealed class AssetTooLargeDialogState { data class Visible(val assetType: AttachmentType, val maxLimitInMB: Int, val savedToDevice: Boolean) : AssetTooLargeDialogState() } +sealed class VisitLinkDialogState { + object Hidden : VisitLinkDialogState() + data class Visible(val link: String, val openLink: () -> Unit) : VisitLinkDialogState() +} + sealed class InvalidLinkDialogState { object Hidden : InvalidLinkDialogState() object Visible : InvalidLinkDialogState() } + 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 24a56e9de1d..8ea26c58b9d 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 @@ -67,7 +67,7 @@ fun MarkdownText( start = offset, end = offset, ).firstOrNull()?.let { result -> - onClickLink?.invoke(annotatedString.substring(result.start, result.end)) + onClickLink?.invoke(result.item) } annotatedString.getStringAnnotations( diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt new file mode 100644 index 00000000000..c2036596c8c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt @@ -0,0 +1,36 @@ +/* + * 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 java.net.URLDecoder + +fun containsSchema(url: String): Boolean { + val regexPattern = Regex(".+://.+") + + return regexPattern.matches(url) +} + +fun normalizeLink(url: String): String { + val normalizedUrl = URLDecoder.decode(url, "UTF-8") // Decode URL to human-readable format + + return if (containsSchema(normalizedUrl)) { + normalizedUrl + } else { + "https://$normalizedUrl" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d69d9839011..49b527355fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ New Login OK + Open Cancel Confirm Continue @@ -990,9 +991,13 @@ When this is on, all messages in this group will disappear after a certain time. This applies to all group participants. Timer + + Visit Link + This will take you to %s + Invalid Link - The schema of the embedded link is not valid. + Link could not be opened Guest link diff --git a/app/src/test/kotlin/com/wire/android/TestUtil.kt b/app/src/test/kotlin/com/wire/android/TestUtil.kt new file mode 100644 index 00000000000..97768eac750 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/TestUtil.kt @@ -0,0 +1,26 @@ +/* + * 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 + +import kotlin.random.Random + +val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + +fun Random.string(length: Int) = (1..length) + .map { Random.nextInt(0, charPool.size).let { charPool[it] } } + .joinToString("") diff --git a/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt b/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt new file mode 100644 index 00000000000..9433bb51198 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt @@ -0,0 +1,57 @@ +/* + * 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 com.wire.android.string +import org.junit.jupiter.api.Test +import kotlin.random.Random + +class UriUtilTest { + @Test + fun givenLink_whenTheLinkStartsWithHttps_thenReturnsTheSameLink() { + val input = "https://google.com" + val expected = "https://google.com" + val actual = normalizeLink(input) + assert(expected == actual) + } + + @Test + fun givenLink_whenTheLinkStartsWithHttp_thenReturnsTheSameLink() { + val input = "http://google.com" + val expected = "http://google.com" + val actual = normalizeLink(input) + assert(expected == actual) + } + + @Test + fun givenLink_whenTheLinkStartsWithRandomSchema_thenReturnsTheSameLink() { + val randomString = Random.string(Random.nextInt(5)) + val input = "$randomString://google.com" + val expected = "$randomString://google.com" + val actual = normalizeLink(input) + assert(expected == actual) + } + + @Test + fun givenLink_whenTheLinkWithoutSchema_thenReturnsTheLinkWithHttps() { + val input = Random.string(Random.nextInt(20)) + val expected = "https://$input" + val actual = normalizeLink(input) + assert(expected == actual) + } +}