diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index def84b57f..0b36a73ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index ac5ee5004..b93ae4ab6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.service.ExpiringMessageManager; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 9a02166db..42071eb82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -153,8 +153,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) @@ -667,7 +667,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) @@ -683,7 +683,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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index ee93c75ba..4b413ef89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -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(context, cursor) { @@ -72,7 +73,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, searchQuery) - 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index 7d5b29393..e0326723a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -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(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) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java rename to app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java index 65cad0a27..5a04e77ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ExpirationTimerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.conversation.v2.components; import android.content.Context; import androidx.annotation.NonNull; @@ -118,5 +118,4 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn)); } } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index a56392b30..8f0e61b38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -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, searchQuery) + bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) 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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 8e51172f9..16721b162 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -12,6 +12,7 @@ import android.text.util.Linkify import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater +import android.view.MotionEvent import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt @@ -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.* @@ -43,7 +45,7 @@ import java.util.* 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 @@ -75,9 +77,7 @@ class VisibleMessageContentView : LinearLayout { val linkPreviewView = LinkPreviewView(context) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) 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!! @@ -92,10 +92,10 @@ class VisibleMessageContentView : LinearLayout { val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) ViewUtil.setPaddingTop(bodyTextView, 0) mainContainer.addView(bodyTextView) - onContentClick = { rect -> + onContentClick = { event -> val r = Rect() quoteView.getGlobalVisibleRect(r) - if (r.contains(rect)) { + if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) { delegate?.scrollToMessageIfPossible(quote.id) } } @@ -122,7 +122,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)) @@ -131,6 +133,12 @@ class VisibleMessageContentView : LinearLayout { } else { val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) mainContainer.addView(bodyTextView) + onContentClick = { event -> + // intersectedModalSpans should only be a list of one item + bodyTextView.getIntersectedModalSpans(event).forEach { span -> + span.onClick(bodyTextView) + } + } } } @@ -181,10 +189,11 @@ class VisibleMessageContentView : LinearLayout { body.removeSpan(urlSpan) body.setSpan(replacementSpan, start, end, flags) } - - body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context); + + body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context) body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery) body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery) + result.text = body return result } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 20008a5f1..23465fbe3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt index 9bba13400..b7ced4abb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt @@ -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 { + 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 { + 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(off, off).toList() + } else { + emptyList() + } + } + } + } \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_item_footer.xml b/app/src/main/res/layout/conversation_item_footer.xml index 740234f0e..ab16a0930 100644 --- a/app/src/main/res/layout/conversation_item_footer.xml +++ b/app/src/main/res/layout/conversation_item_footer.xml @@ -26,7 +26,7 @@ android:textAllCaps="true" tools:text="30 mins"/> -