Skip to content

Commit

Permalink
fix(message): fix in-message embedded link validator
Browse files Browse the repository at this point in the history
  • Loading branch information
mchenani committed Aug 11, 2023
1 parent cbd5ae1 commit 0d1bd5e
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}
},
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -150,6 +141,10 @@ class MessageComposerViewModel @Inject constructor(
AssetTooLargeDialogState.Hidden
)

var visitLinkDialogState: VisitLinkDialogState by mutableStateOf(
VisitLinkDialogState.Hidden
)

var invalidLinkDialogState: InvalidLinkDialogState by mutableStateOf(
InvalidLinkDialogState.Hidden
)
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/kotlin/com/wire/android/util/UriUtil.kt
Original file line number Diff line number Diff line change
@@ -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"
}
}
7 changes: 6 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<string name="label_new">New</string>
<string name="label_login">Login</string>
<string name="label_ok">OK</string>
<string name="label_open">Open</string>
<string name="label_cancel">Cancel</string>
<string name="label_confirm">Confirm</string>
<string name="label_continue">Continue</string>
Expand Down Expand Up @@ -990,9 +991,13 @@
<string name="self_deleting_messages_option_description">When this is on, all messages in this group will disappear after a certain time. This applies to all group participants.</string>
<string name="self_deleting_messages_folder_timer">Timer</string>

<!-- visit link -->
<string name="label_visit_link_title">Visit Link</string>
<string name="visit_link_dialog_body">This will take you to %s</string>

<!-- invalid link -->
<string name="label_invalid_link_title">Invalid Link</string>
<string name="invalid_link">The schema of the embedded link is not valid.</string>
<string name="invalid_link_dialog_body">Link could not be opened</string>

<!-- guest room link -->
<string name="folder_label_guest_link">Guest link</string>
Expand Down
26 changes: 26 additions & 0 deletions app/src/test/kotlin/com/wire/android/TestUtil.kt
Original file line number Diff line number Diff line change
@@ -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<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')

fun Random.string(length: Int) = (1..length)
.map { Random.nextInt(0, charPool.size).let { charPool[it] } }
.joinToString("")
57 changes: 57 additions & 0 deletions app/src/test/kotlin/com/wire/android/util/UriUtilTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 0d1bd5e

Please sign in to comment.