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)
+ }
+}