mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge pull request #621 from hjubb/ui
Fix Body Text URL Handling & Open Group Media Previews
This commit is contained in:
commit
e91b84f694
@ -118,13 +118,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
private int restartItem = -1;
|
||||
|
||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms) {
|
||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
||||
Intent previewIntent = null;
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
previewIntent = new Intent(context, MediaPreviewActivity.class);
|
||||
previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setDataAndType(slide.getUri(), slide.getContentType())
|
||||
.putExtra(ADDRESS_EXTRA, mms.getRecipient().getAddress())
|
||||
.putExtra(ADDRESS_EXTRA, threadRecipient.getAddress())
|
||||
.putExtra(OUTGOING_EXTRA, mms.isOutgoing())
|
||||
.putExtra(DATE_EXTRA, mms.getTimestamp())
|
||||
.putExtra(SIZE_EXTRA, slide.asAttachment().getSize())
|
||||
|
@ -143,8 +143,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val adapter = ConversationAdapter(
|
||||
this,
|
||||
cursor,
|
||||
onItemPress = { message, position, view, rawRect ->
|
||||
handlePress(message, position, view, rawRect)
|
||||
onItemPress = { message, position, view, event ->
|
||||
handlePress(message, position, view, event)
|
||||
},
|
||||
onItemSwipeToReply = { message, position ->
|
||||
handleSwipeToReply(message, position)
|
||||
@ -655,7 +655,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, rawRect: Rect) {
|
||||
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, event: MotionEvent) {
|
||||
val actionMode = this.actionMode
|
||||
if (actionMode != null) {
|
||||
adapter.toggleSelection(message, position)
|
||||
@ -671,7 +671,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// We have to use onContentClick (rather than a click listener directly on
|
||||
// the view) so as to not interfere with all the other gestures. Do not add
|
||||
// onClickListeners directly to message content views.
|
||||
view.onContentClick(rawRect)
|
||||
view.onContentClick(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Rect
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, Rect) -> Unit,
|
||||
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
|
||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
|
||||
private val glide: GlideRequests)
|
||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
@ -71,7 +72,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
view.messageTimestampTextView.isVisible = isSelected
|
||||
val position = viewHolder.adapterPosition
|
||||
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide)
|
||||
view.onPress = { rawX, rawY -> onItemPress(message, viewHolder.adapterPosition, view, Rect(rawX, rawY, rawX, rawY)) }
|
||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||
|
@ -5,6 +5,7 @@ import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
@ -13,6 +14,7 @@ import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.ViewUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||
@ -58,12 +60,15 @@ class AlbumThumbnailView : FrameLayout {
|
||||
|
||||
// region Interaction
|
||||
|
||||
fun calculateHitObject(rawRect: Rect, mms: MmsMessageRecord) {
|
||||
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
// Z-check in specific order
|
||||
val testRect = Rect()
|
||||
// test "Read More"
|
||||
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
||||
if (Rect.intersects(rawRect, testRect)) {
|
||||
if (testRect.contains(eventRect)) {
|
||||
// dispatch to activity view
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
|
||||
@ -73,14 +78,14 @@ class AlbumThumbnailView : FrameLayout {
|
||||
// test each album child
|
||||
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||
child.getGlobalVisibleRect(testRect)
|
||||
if (Rect.intersects(rawRect, testRect)) {
|
||||
if (testRect.contains(eventRect)) {
|
||||
// hit intersects with this particular child
|
||||
val slide = slides.getOrNull(index) ?: return
|
||||
// only open to downloaded images
|
||||
if (slide.isInProgress) return
|
||||
|
||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||
MediaPreviewActivity.getPreviewIntent(context, slide, mms)
|
||||
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,21 @@ import android.graphics.Rect
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.view_link_preview.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
@ -23,6 +29,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
class LinkPreviewView : LinearLayout {
|
||||
private val cornerMask by lazy { CornerMask(this) }
|
||||
private var url: String? = null
|
||||
lateinit var bodyTextView: TextView
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@ -53,7 +60,7 @@ class LinkPreviewView : LinearLayout {
|
||||
}
|
||||
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
// Body
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
mainLinkPreviewContainer.addView(bodyTextView)
|
||||
// Corner radii
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
@ -70,11 +77,20 @@ class LinkPreviewView : LinearLayout {
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
fun calculateHit(hitRect: Rect) {
|
||||
fun calculateHit(event: MotionEvent) {
|
||||
val rawXInt = event.rawX.toInt()
|
||||
val rawYInt = event.rawY.toInt()
|
||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||
val previewRect = Rect()
|
||||
mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
||||
if (previewRect.contains(hitRect)) {
|
||||
openURL()
|
||||
return
|
||||
}
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect)
|
||||
hitSpans.forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
|
||||
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.loki.utilities.*
|
||||
@ -40,7 +42,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
var onContentClick: ((rawRect: Rect) -> Unit)? = null
|
||||
var onContentClick: ((event: MotionEvent) -> Unit)? = null
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
var delegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
@ -72,9 +74,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
val linkPreviewView = LinkPreviewView(context)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
mainContainer.addView(linkPreviewView)
|
||||
onContentClick = { rect ->
|
||||
linkPreviewView.calculateHit(rect)
|
||||
}
|
||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
||||
// Body text view is inside the link preview for layout convenience
|
||||
} else if (message is MmsMessageRecord && message.quote != null) {
|
||||
val quote = message.quote!!
|
||||
@ -119,7 +119,9 @@ class VisibleMessageContentView : LinearLayout {
|
||||
isStart = isStartOfMessageCluster,
|
||||
isEnd = isEndOfMessageCluster
|
||||
)
|
||||
onContentClick = { albumThumbnailView.calculateHitObject(it, message) }
|
||||
onContentClick = { event ->
|
||||
albumThumbnailView.calculateHitObject(event, message, thread)
|
||||
}
|
||||
} else if (message.isOpenGroupInvitation) {
|
||||
val openGroupInvitationView = OpenGroupInvitationView(context)
|
||||
openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message))
|
||||
@ -128,6 +130,12 @@ class VisibleMessageContentView : LinearLayout {
|
||||
} else {
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
|
||||
span.onClick(bodyTextView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +187,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
body.setSpan(replacementSpan, start, end, flags)
|
||||
}
|
||||
|
||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context);
|
||||
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
|
||||
result.text = body
|
||||
return result
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class VisibleMessageView : LinearLayout {
|
||||
private var onDoubleTap: (() -> Unit)? = null
|
||||
var snIsSelected = false
|
||||
set(value) { field = value; handleIsSelectedChanged()}
|
||||
var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null
|
||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||
var onSwipeToReply: (() -> Unit)? = null
|
||||
var onLongPress: (() -> Unit)? = null
|
||||
var contentViewDelegate: VisibleMessageContentViewDelegate? = null
|
||||
@ -280,7 +280,7 @@ class VisibleMessageView : LinearLayout {
|
||||
this.pressCallback = null
|
||||
onDoubleTap?.invoke()
|
||||
} else {
|
||||
val newPressCallback = Runnable { onPress(event.rawX.toInt(), event.rawY.toInt()) }
|
||||
val newPressCallback = Runnable { onPress(event) }
|
||||
this.pressCallback = newPressCallback
|
||||
gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval)
|
||||
}
|
||||
@ -308,12 +308,12 @@ class VisibleMessageView : LinearLayout {
|
||||
onLongPress?.invoke()
|
||||
}
|
||||
|
||||
fun onContentClick(rawRect: Rect) {
|
||||
messageContentView.onContentClick?.invoke(rawRect)
|
||||
fun onContentClick(event: MotionEvent) {
|
||||
messageContentView.onContentClick?.invoke(event)
|
||||
}
|
||||
|
||||
private fun onPress(rawX: Int, rawY: Int) {
|
||||
onPress?.invoke(rawX, rawY)
|
||||
private fun onPress(event: MotionEvent) {
|
||||
onPress?.invoke(event)
|
||||
pressCallback = null
|
||||
}
|
||||
// endregion
|
||||
|
@ -1,8 +1,13 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.view.MotionEvent
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
|
||||
object TextUtilities {
|
||||
|
||||
@ -14,4 +19,31 @@ object TextUtilities {
|
||||
val layout = builder.build()
|
||||
return layout.height
|
||||
}
|
||||
|
||||
fun TextView.getIntersectedModalSpans(event: MotionEvent): List<ModalURLSpan> {
|
||||
val xInt = event.rawX.toInt()
|
||||
val yInt = event.rawY.toInt()
|
||||
val hitRect = Rect(xInt, yInt, xInt, yInt)
|
||||
return getIntersectedModalSpans(hitRect)
|
||||
}
|
||||
|
||||
fun TextView.getIntersectedModalSpans(hitRect: Rect): List<ModalURLSpan> {
|
||||
val textLayout = layout ?: return emptyList()
|
||||
val lineRect = Rect()
|
||||
val bodyTextRect = Rect()
|
||||
getGlobalVisibleRect(bodyTextRect)
|
||||
val textSpannable = text.toSpannable()
|
||||
return (0 until textLayout.lineCount).flatMap { line ->
|
||||
textLayout.getLineBounds(line, lineRect)
|
||||
lineRect.offset(bodyTextRect.left + totalPaddingLeft, bodyTextRect.top + totalPaddingTop)
|
||||
if ((Rect(lineRect)).contains(hitRect)) {
|
||||
// calculate the url span intersected with (if any)
|
||||
val off = textLayout.getOffsetForHorizontal(line, hitRect.left.toFloat()) // left and right will be the same
|
||||
textSpannable.getSpans<ModalURLSpan>(off, off).toList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user