Proper set up for the Open URL dialog

This commit is contained in:
ThomasSession 2024-08-28 15:13:58 +10:00
parent 2aa58f4dd6
commit 25132c6342
6 changed files with 143 additions and 116 deletions

View File

@ -19,10 +19,7 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.text.style.StyleSpan
import android.util.Pair import android.util.Pair
import android.util.TypedValue import android.util.TypedValue
import android.view.ActionMode import android.view.ActionMode
@ -36,6 +33,10 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.drawToBitmap import androidx.core.view.drawToBitmap
import androidx.core.view.isGone import androidx.core.view.isGone
@ -170,6 +171,8 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
@ -195,6 +198,7 @@ import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
private const val TAG = "ConversationActivityV2" private const val TAG = "ConversationActivityV2"
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually // Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
@ -237,6 +241,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java) .get(LinkPreviewViewModel::class.java)
} }
private var openLinkDialogUrl: String? by mutableStateOf(null)
private val threadId: Long by lazy { private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L) var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) { if (threadId == -1L) {
@ -400,12 +406,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// endregion // endregion
fun showOpenUrlDialog(url: String){
openLinkDialogUrl = url
}
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater) binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// set the compose dialog content
binding.dialogOpenUrl.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SessionMaterialTheme {
if(!openLinkDialogUrl.isNullOrEmpty()){
OpenURLAlertDialog(
url = openLinkDialogUrl,
onDismissRequest = {
openLinkDialogUrl = null
}
)
}
}
}
}
// messageIdToScroll // messageIdToScroll
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))

View File

@ -11,47 +11,32 @@ import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.copyURLToClipboard
import org.thoughtcrime.securesms.openUrl
import org.thoughtcrime.securesms.showOpenUrlDialog import org.thoughtcrime.securesms.showOpenUrlDialog
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.getAccentColor
@ -71,7 +56,6 @@ class VisibleMessageContentView : ConstraintLayout {
// endregion // endregion
// region Updating // region Updating
@Composable
fun bind( fun bind(
message: MessageRecord, message: MessageRecord,
isStartOfMessageCluster: Boolean = true, isStartOfMessageCluster: Boolean = true,
@ -285,7 +269,6 @@ class VisibleMessageContentView : ConstraintLayout {
// region Convenience // region Convenience
companion object { companion object {
@Composable
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable { fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
var body = message.body.toSpannable() var body = message.body.toSpannable()
@ -306,14 +289,8 @@ class VisibleMessageContentView : ConstraintLayout {
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan -> body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() } val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
val replacementSpan = ModalURLSpan(updatedUrl) { url -> val replacementSpan = ModalURLSpan(updatedUrl) { url ->
val activity = context as ConversationActivityV2
// @Thomas - PREVIOUS CODE WAS: activity.showOpenUrlDialog(url)
//val activity = context as AppCompatActivity
//activity.showOpenUrlDialog(url)
// Now attempting to use compose for the dialog - but it's not happy =/
OpenURLWarningDialog(url)
} }
val start = body.getSpanStart(urlSpan) val start = body.getSpanStart(urlSpan)
val end = body.getSpanEnd(urlSpan) val end = body.getSpanEnd(urlSpan)
@ -324,35 +301,6 @@ class VisibleMessageContentView : ConstraintLayout {
return body return body
} }
@Composable
private fun OpenURLWarningDialog(url: String) {
val context = LocalContext.current
OpenURLAlertDialog(
title = stringResource(R.string.urlOpen),
text = stringResource(R.string.urlOpenDescription),
showCloseButton = true, // display the 'x' button
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.open),
contentDescription = GetString(R.string.AccessibilityId_urlOpenBrowser),
color = LocalColors.current.danger,
onClick = { context.openUrl(url) }
),
DialogButtonModel(
text = GetString(android.R.string.copyUrl),
contentDescription = GetString(R.string.AccessibilityId_copy),
onClick = {
context.copyURLToClipboard(url)
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
}
)
),
url = url,
onDismissRequest = {}
)
}
@ColorInt @ColorInt
fun getTextColor(context: Context, message: MessageRecord): Int = context.getColorFromAttr( fun getTextColor(context: Context, message: MessageRecord): Int = context.getColorFromAttr(
if (message.isOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color if (message.isOutgoing) R.attr.message_sent_text_color else R.attr.message_received_text_color

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.ui package org.thoughtcrime.securesms.ui
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -12,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -25,14 +28,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.times
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
import org.thoughtcrime.securesms.copyURLToClipboard
import org.thoughtcrime.securesms.openUrl
import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
@ -48,17 +61,40 @@ class DialogButtonModel(
val onClick: () -> Unit = {}, val onClick: () -> Unit = {},
) )
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AlertDialog( fun AlertDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null, title: String? = null,
text: String? = null, text: String? = null,
maxLines: Int? = null,
buttons: List<DialogButtonModel>? = null, buttons: List<DialogButtonModel>? = null,
showCloseButton: Boolean = false, showCloseButton: Boolean = false,
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}
optionalURL: String = "" ) {
AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier,
title = if(title != null) AnnotatedString(title) else null,
text = if(text != null) AnnotatedString(text) else null,
maxLines = maxLines,
buttons = buttons,
showCloseButton = showCloseButton,
content = content
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlertDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: AnnotatedString? = null,
text: AnnotatedString? = null,
maxLines: Int? = null,
buttons: List<DialogButtonModel>? = null,
showCloseButton: Boolean = false,
content: @Composable () -> Unit = {}
) { ) {
BasicAlertDialog( BasicAlertDialog(
modifier = modifier, modifier = modifier,
@ -96,25 +132,26 @@ fun AlertDialog(
) )
} }
text?.let { text?.let {
if (optionalURL.isNotEmpty()) { val textStyle = LocalType.current.large
// If this is an open URL dialog it should have a maximum height of 5 lines and truncate long URLs with an ellipsis var textModifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
Text(
text = it, // if we have a maxLines, make the text scrollable
textAlign = TextAlign.Center, if(maxLines != null) {
style = LocalType.current.large, val textHeight = with(LocalDensity.current) {
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing), textStyle.lineHeight.toDp()
maxLines = 5, } * maxLines
overflow = TextOverflow.Ellipsis
) textModifier = textModifier
} else { .height(textHeight)
// Otherwise it should be a regular, non-open-URL dialog .verticalScroll(rememberScrollState())
Text(
text = it,
textAlign = TextAlign.Center,
style = LocalType.current.large,
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
)
} }
Text(
text = it,
textAlign = TextAlign.Center,
style = textStyle,
modifier = textModifier
)
} }
content() content()
} }
@ -141,27 +178,42 @@ fun AlertDialog(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun OpenURLAlertDialog( fun OpenURLAlertDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String? = null, url: String,
text: String? = null, content: @Composable () -> Unit = {}
buttons: List<DialogButtonModel>? = null,
showCloseButton: Boolean = false,
content: @Composable () -> Unit = {},
url: String = ""
) { ) {
val context = LocalContext.current
val unformattedText = Phrase.from(context.getText(R.string.urlOpenDescription))
.put(URL_KEY, url).format()
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier, modifier = modifier,
title = title, title = AnnotatedString(stringResource(R.string.urlOpen)),
text = text, text = annotatedStringResource(text = unformattedText),
buttons = buttons, maxLines = 5,
showCloseButton = showCloseButton, showCloseButton = true, // display the 'x' button
content = content, buttons = listOf(
optionalURL = url DialogButtonModel(
text = GetString(R.string.open),
contentDescription = GetString(R.string.AccessibilityId_urlOpenBrowser),
color = LocalColors.current.danger,
onClick = { context.openUrl(url) }
),
DialogButtonModel(
text = GetString(android.R.string.copyUrl),
contentDescription = GetString(R.string.AccessibilityId_copy),
onClick = {
context.copyURLToClipboard(url)
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
}
)
),
onDismissRequest = onDismissRequest,
content = content
) )
} }
@ -255,15 +307,14 @@ fun PreviewSimpleDialog() {
text = stringResource(R.string.onboardingBackAccountCreation), text = stringResource(R.string.onboardingBackAccountCreation),
buttons = listOf( buttons = listOf(
DialogButtonModel( DialogButtonModel(
GetString(stringResource(R.string.quit)), GetString(stringResource(R.string.cancel)),
color = LocalColors.current.danger, color = LocalColors.current.danger,
onClick = { } onClick = { }
), ),
DialogButtonModel( DialogButtonModel(
GetString(stringResource(R.string.cancel)) GetString(stringResource(R.string.ok))
) )
), )
optionalURL = "https://slashdot.org"
) )
} }
} }
@ -298,23 +349,7 @@ fun PreviewXCloseDialog() {
fun PreviewOpenURLDialog() { fun PreviewOpenURLDialog() {
PreviewTheme { PreviewTheme {
OpenURLAlertDialog( OpenURLAlertDialog(
title = stringResource(R.string.urlOpen), url = "https://getsession.org/",
text = stringResource(R.string.urlOpenDescription),
showCloseButton = true, // display the 'x' button
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.open),
contentDescription = GetString(R.string.AccessibilityId_urlOpenBrowser),
color = LocalColors.current.danger,
onClick = {}
),
DialogButtonModel(
text = GetString(android.R.string.copyUrl),
contentDescription = GetString(R.string.AccessibilityId_copy),
onClick = {}
)
),
url = "http://slashdot.org",
onDismissRequest = {} onDismissRequest = {}
) )
} }

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
@ -26,6 +27,9 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
// TODO Remove this file once we update to composeVersion=1.7.0-alpha06 fixes https://issuetracker.google.com/issues/139320238?pli=1 // TODO Remove this file once we update to composeVersion=1.7.0-alpha06 fixes https://issuetracker.google.com/issues/139320238?pli=1
// which allows Stylized string in string resources // which allows Stylized string in string resources
@ -71,6 +75,14 @@ fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
} }
} }
@Composable
fun annotatedStringResource(text: CharSequence): AnnotatedString {
val density = LocalDensity.current
return remember(text.hashCode()) {
spannableStringToAnnotatedString(text, density)
}
}
private fun spannableStringToAnnotatedString( private fun spannableStringToAnnotatedString(
text: CharSequence, text: CharSequence,
density: Density density: Density

View File

@ -346,4 +346,9 @@
</LinearLayout> </LinearLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_open_url"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -792,7 +792,7 @@ NOTE: Strings with blank lines have manually been replaced with '\n\n' - this wi
<string name="urlCopy">Copy URL</string> <string name="urlCopy">Copy URL</string>
<string name="urlOpen">Open URL</string> <string name="urlOpen">Open URL</string>
<string name="urlOpenBrowser">This will open in your browser.</string> <string name="urlOpenBrowser">This will open in your browser.</string>
<string name="urlOpenDescription">Are you sure you want to open this URL in your browser?\n\n{url}</string> <string name="urlOpenDescription">Are you sure you want to open this URL in your browser?\n\n<b>{url}</b></string>
<string name="useFastMode">Use Fast Mode</string> <string name="useFastMode">Use Fast Mode</string>
<string name="video">Video</string> <string name="video">Video</string>
<string name="videoErrorPlay">Unable to play video.</string> <string name="videoErrorPlay">Unable to play video.</string>