This commit is contained in:
ryanzhao 2021-06-30 14:55:30 +10:00
commit 79fd74a157
11 changed files with 95 additions and 32 deletions

View File

@ -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())

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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) {
@ -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

View File

@ -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)
}
}
}

View File

@ -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));
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}
@ -182,9 +190,10 @@ 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)
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
}

View File

@ -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

View File

@ -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()
}
}
}
}

View File

@ -26,7 +26,7 @@
android:textAllCaps="true"
tools:text="30 mins"/>
<org.thoughtcrime.securesms.components.ExpirationTimerView
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/footer_expiration_timer"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="6dp"