Merge branch 'compose-open-url-dialog' into strings-squashed

This commit is contained in:
alansley 2024-08-28 16:31:43 +10:00
commit 2a8f010369
8 changed files with 164 additions and 158 deletions

View File

@ -4,25 +4,16 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.LinearLayout.VERTICAL
import android.widget.RelativeLayout
import android.widget.ScrollView
import android.widget.Space
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
@ -31,9 +22,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
import org.thoughtcrime.securesms.util.toPx
@DslMarker
@ -199,127 +188,6 @@ public fun Context.copyURLToClipboard(url: String) {
clipboard.setPrimaryClip(clip)
}
// Method to show a dialog used to open or copy a URL
fun Context.showOpenUrlDialog(url: String, showCloseButton: Boolean = true): AlertDialog {
return SessionDialogBuilder(this).apply {
// If we're not showing a close button we can just use a simple title..
if (!showCloseButton) {
title(R.string.urlOpen)
} else {
// ..otherwise we have to jump through some hoops to add a close button.
// Create a RelativeLayout as the container for the custom title
val titleLayout = RelativeLayout(context).apply {
layoutParams = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
)
}
// Create a TextView for the title
val titleTextView = TextView(context).apply {
// Set the text and display it in the correct 'title' style
text = context.getString(R.string.urlOpen)
setTextAppearance(R.style.TextAppearance_AppCompat_Title)
layoutParams = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.CENTER_HORIZONTAL)
addRule(RelativeLayout.CENTER_VERTICAL)
}
}
// Create an ImageButton for the close button
val closeButton = ImageButton(context).apply {
setImageResource(android.R.drawable.ic_menu_close_clear_cancel) // Use a standard Android close icon
background = null // Remove the background to make it look like an icon
layoutParams = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
).apply {
addRule(RelativeLayout.ALIGN_PARENT_END) // Place the close button on the "right" side
addRule(RelativeLayout.CENTER_VERTICAL)
}
contentDescription = context.getString(R.string.close)
}
// // Close the dialog when the button is clicked
closeButton.setOnClickListener { dismiss() }
// Add the TextView and ImageButton to the RelativeLayout..
titleLayout.addView(titleTextView)
titleLayout.addView(closeButton)
// ..and then add that layout to the contentView.
contentView.addView(titleLayout)
}
// Create a TextView for the "Are you sure you want to open this URL?"
val txtView = TextView(context).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, 0) }
// Substitute the URL into the string then make it bold
val txt = Phrase.from(context, R.string.urlOpenDescription).put(URL_KEY, url).format().toString()
val txtWithBoldedURL = SpannableString(txt)
val urlStart = txt.indexOf(url)
if (urlStart >= 0) {
txtWithBoldedURL.setSpan(
StyleSpan(Typeface.BOLD),
urlStart,
urlStart + url.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
text = txtWithBoldedURL
gravity = Gravity.CENTER // Center the text
}
// Create a ScrollView and add the TextView to it
val scrollView = ScrollView(context).apply {
addView(txtView)
// Apply padding to the ScrollView so that the scroll bar isn't right up against the edge.
// We'll apply the same padding to both sides to keep the text centered.
setPadding(dp20, 0, dp20, 0)
// Place the scroll bar inside the container.
// See the following for how different options look: https://stackoverflow.com/questions/3103132/android-listview-scrollbarstyle
scrollBarStyle = ScrollView.SCROLLBARS_INSIDE_INSET
}
// If the textView takes up 5 lines or more then show the scroll bar, force it to remain visible,
// and set the ScrollView height accordingly.
txtView.viewTreeObserver.addOnGlobalLayoutListener {
// Only display the vertical scroll bar if the text takes up 5 lines or more
val maxLines = 5
if (txtView.lineCount >= maxLines) {
scrollView.isVerticalScrollBarEnabled = true
// Note: `scrollView.isScrollbarFadingEnabled = false` does NOT
// work to prevent the scroll bar from fading away - so a hacky
// way to fix this is to allow it to fade out... after an hour, lol.
scrollView.scrollBarFadeDuration = 1000 * 60 * 60 // Value is in milliseconds
scrollView.isVerticalFadingEdgeEnabled = false
val lineHeight = txtView.lineHeight
scrollView.layoutParams.height = lineHeight * maxLines
}
}
// Add the ScrollView to the contentView and then add the 'Open' and 'Copy URL' buttons.
// Note: The text and contentDescription are set on the `copyUrlButton` by the function.
contentView.addView(scrollView)
dangerButton(R.string.open, R.string.AccessibilityId_urlOpenBrowser) { openUrl(url) }
copyUrlButton {
context.copyURLToClipboard(url)
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
}
}.show()
}
// Method to actually open a given URL via an Intent that will use the default browser
fun Context.openUrl(url: String) = Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity)

View File

@ -19,10 +19,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import android.text.style.StyleSpan
import android.util.Pair
import android.util.TypedValue
import android.view.ActionMode
@ -36,6 +33,10 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.view.drawToBitmap
import androidx.core.view.isGone
@ -171,6 +172,8 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
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.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
@ -196,6 +199,7 @@ import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.minutes
private const val TAG = "ConversationActivityV2"
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
@ -238,6 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java)
}
private var openLinkDialogUrl: String? by mutableStateOf(null)
private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
@ -401,12 +407,33 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
// endregion
fun showOpenUrlDialog(url: String){
openLinkDialogUrl = url
}
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater)
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
messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1))
messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR))

View File

@ -6,7 +6,6 @@ import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.bumptech.glide.RequestManager
import network.loki.messenger.R
@ -14,10 +13,10 @@ import network.loki.messenger.databinding.ViewLinkPreviewBinding
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.showOpenUrlDialog
class LinkPreviewView : LinearLayout {
private val binding: ViewLinkPreviewBinding by lazy { ViewLinkPreviewBinding.bind(this) }
@ -88,8 +87,8 @@ class LinkPreviewView : LinearLayout {
// Method to show the open or copy URL dialog
private fun openURL() {
val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL")
val activity = context as AppCompatActivity
activity.showOpenUrlDialog(url)
val activity = context as? ConversationActivityV2
activity?.showOpenUrlDialog(url)
}
// endregion
}

View File

@ -12,35 +12,29 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.view.children
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import okhttp3.HttpUrl
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.utilities.ThemeUtil
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsession.utilities.recipients.Recipient
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.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.showOpenUrlDialog
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.getAccentColor
@ -293,8 +287,8 @@ class VisibleMessageContentView : ConstraintLayout {
body.getSpans<URLSpan>(0, body.length).toList().forEach { urlSpan ->
val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() }
val replacementSpan = ModalURLSpan(updatedUrl) { url ->
val activity = context as AppCompatActivity
activity.showOpenUrlDialog(url)
val activity = context as? ConversationActivityV2
activity?.showOpenUrlDialog(url)
}
val start = body.getSpanStart(urlSpan)
val end = body.getSpanEnd(urlSpan)

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.ui
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
@ -12,7 +13,8 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -26,13 +28,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
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.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
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 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.annotatedStringResource
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
@ -48,13 +61,37 @@ class DialogButtonModel(
val onClick: () -> Unit = {},
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlertDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
text: String? = null,
maxLines: Int? = null,
buttons: List<DialogButtonModel>? = null,
showCloseButton: Boolean = false,
content: @Composable () -> Unit = {}
) {
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 = {}
@ -88,18 +125,32 @@ fun AlertDialog(
) {
title?.let {
Text(
it,
text = it,
textAlign = TextAlign.Center,
style = LocalType.current.h7,
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
)
}
text?.let {
val textStyle = LocalType.current.large
var textModifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
// if we have a maxLines, make the text scrollable
if(maxLines != null) {
val textHeight = with(LocalDensity.current) {
textStyle.lineHeight.toDp()
} * maxLines
textModifier = textModifier
.height(textHeight)
.verticalScroll(rememberScrollState())
}
Text(
it,
text = it,
textAlign = TextAlign.Center,
style = LocalType.current.large,
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
style = textStyle,
modifier = textModifier
)
}
content()
@ -127,6 +178,45 @@ fun AlertDialog(
)
}
@Composable
fun OpenURLAlertDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
url: String,
content: @Composable () -> Unit = {}
) {
val context = LocalContext.current
val unformattedText = Phrase.from(context.getText(R.string.urlOpenDescription))
.put(URL_KEY, url).format()
AlertDialog(
modifier = modifier,
title = AnnotatedString(stringResource(R.string.urlOpen)),
text = annotatedStringResource(text = unformattedText),
maxLines = 5,
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()
}
)
),
onDismissRequest = onDismissRequest,
content = content
)
}
@Composable
fun DialogButton(
text: String,
@ -217,12 +307,12 @@ fun PreviewSimpleDialog() {
text = stringResource(R.string.onboardingBackAccountCreation),
buttons = listOf(
DialogButtonModel(
GetString(stringResource(R.string.quit)),
GetString(stringResource(R.string.cancel)),
color = LocalColors.current.danger,
onClick = { }
),
DialogButtonModel(
GetString(stringResource(R.string.cancel))
GetString(stringResource(R.string.ok))
)
)
)
@ -254,6 +344,17 @@ fun PreviewXCloseDialog() {
}
}
@Preview
@Composable
fun PreviewOpenURLDialog() {
PreviewTheme {
OpenURLAlertDialog(
url = "https://getsession.org/",
onDismissRequest = {}
)
}
}
@Preview
@Composable
fun PreviewLoadingDialog() {

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
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.em
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
// 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(
text: CharSequence,
density: Density

View File

@ -346,4 +346,9 @@
</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>

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="urlOpen">Open URL</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="video">Video</string>
<string name="videoErrorPlay">Unable to play video.</string>