Scroll to message upon tapping quote & fix various bugs

pull/597/head
Niels Andriesse 3 years ago
parent 7ce124118f
commit bef7413055

@ -3,15 +3,14 @@ package org.thoughtcrime.securesms.conversation.v2
import android.Manifest import android.Manifest
import android.animation.FloatEvaluator import android.animation.FloatEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.text.TextUtils import android.text.TextUtils
@ -72,6 +71,7 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCand
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -84,11 +84,11 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities import org.thoughtcrime.securesms.loki.utilities.MentionUtilities
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
@ -108,7 +108,7 @@ import kotlin.math.*
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate { ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var linkPreviewViewModel: LinkPreviewViewModel? = null private var linkPreviewViewModel: LinkPreviewViewModel? = null
private var threadID: Long = -1 private var threadID: Long = -1
@ -147,6 +147,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}, },
glide glide
) )
adapter.visibleMessageContentViewDelegate = this
adapter adapter
} }
@ -737,6 +738,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.previousText = newText this.previousText = newText
} }
override fun scrollToMessageIfPossible(timestamp: Long) {
val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
override fun sendMessage() { override fun sendMessage() {
if (thread.isContactRecipient && thread.isBlocked) { if (thread.isContactRecipient && thread.isBlocked) {
BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog") BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog")
@ -905,10 +911,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun startRecordingVoiceMessage() { override fun startRecordingVoiceMessage() {
showVoiceMessageUI() if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) showVoiceMessageUI()
audioRecorder.startRecording() window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each audioRecorder.startRecording()
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
} else {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
.execute()
}
} }
override fun sendVoiceMessage() { override fun sendVoiceMessage() {

@ -8,6 +8,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.*
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -20,6 +21,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) { : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context) private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
var selectedItems = mutableSetOf<MessageRecord>() var selectedItems = mutableSetOf<MessageRecord>()
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
sealed class ViewType(val rawValue: Int) { sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0) object Visible : ViewType(0)
@ -72,6 +74,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.onPress = { rawX, rawY -> onItemPress(message, viewHolder.adapterPosition, view, Rect(rawX, rawY, rawX, rawY)) } view.onPress = { rawX, rawY -> onItemPress(message, viewHolder.adapterPosition, view, Rect(rawX, rawY, rawX, rawY)) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
view.contentViewDelegate = visibleMessageContentViewDelegate
} }
is ControlMessageViewHolder -> viewHolder.view.bind(message) is ControlMessageViewHolder -> viewHolder.view.bind(message)
} }
@ -113,8 +116,19 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
cursor.moveToPosition(i) cursor.moveToPosition(i)
val messageRecord = messageDB.readerFor(cursor).current val message = messageDB.readerFor(cursor).current
if (messageRecord.isOutgoing || messageRecord.dateReceived <= lastSeenTimestamp) { return i } if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i }
}
return null
}
fun getItemPositionForTimestamp(timestamp: Long): Int? {
val cursor = this.cursor
if (timestamp <= 0L || cursor == null || !isActiveCursor) return null
for (i in 0 until itemCount) {
cursor.moveToPosition(i)
val message = messageDB.readerFor(cursor).current
if (message.dateSent == timestamp) { return i }
} }
return null return null
} }

@ -6,10 +6,12 @@ import android.text.SpannableStringBuilder
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.dialog_join_open_group.view.* import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
@ -33,8 +35,11 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
private fun join() { private fun join() {
val openGroup = OpenGroupUrlParser.parseUrl(url) val openGroup = OpenGroupUrlParser.parseUrl(url)
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, requireContext()) val activity = requireContext() as AppCompatActivity
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()) ThreadUtils.queue {
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(activity)
}
dismiss() dismiss()
} }
} }

@ -32,6 +32,9 @@ class ControlMessageView : LinearLayout {
if (message.isExpirationTimerUpdate) { if (message.isExpirationTimerUpdate) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)) iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme))
iconImageView.visibility = View.VISIBLE iconImageView.visibility = View.VISIBLE
} else if (message.isMediaSavedNotification) {
iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme))
iconImageView.visibility = View.VISIBLE
} }
textView.text = message.getDisplayBody(context) textView.text = message.getDisplayBody(context)
} }

@ -23,6 +23,7 @@ import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans import androidx.core.text.getSpans
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import kotlinx.android.synthetic.main.view_link_preview.view.*
import kotlinx.android.synthetic.main.view_visible_message_content.view.* import kotlinx.android.synthetic.main.view_visible_message_content.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ThemeUtil
@ -41,6 +42,7 @@ import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
var onContentClick: ((rawRect: Rect) -> Unit)? = null var onContentClick: ((rawRect: Rect) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -87,6 +89,13 @@ class VisibleMessageContentView : LinearLayout {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { rect ->
val r = Rect()
quoteView.getGlobalVisibleRect(r)
if (r.contains(rect)) {
delegate?.scrollToMessageIfPossible(quote.id)
}
}
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
val voiceMessageView = VoiceMessageView(context) val voiceMessageView = VoiceMessageView(context)
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
@ -188,4 +197,9 @@ class VisibleMessageContentView : LinearLayout {
} }
} }
// endregion // endregion
}
interface VisibleMessageContentViewDelegate {
fun scrollToMessageIfPossible(timestamp: Long)
} }

@ -48,6 +48,7 @@ class VisibleMessageView : LinearLayout {
var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
var contentViewDelegate: VisibleMessageContentViewDelegate? = null
companion object { companion object {
const val swipeToReplyThreshold = 80.0f // dp const val swipeToReplyThreshold = 80.0f // dp
@ -139,6 +140,7 @@ class VisibleMessageView : LinearLayout {
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view // Populate content view
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread) messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread)
messageContentView.delegate = contentViewDelegate
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
} }
@ -239,6 +241,7 @@ class VisibleMessageView : LinearLayout {
} else { } else {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
} }
if (translationX > 0) { return } // Only allow swipes to the left
// The idea here is to asymptotically approach a maximum drag distance // The idea here is to asymptotically approach a maximum drag distance
val damping = 50.0f val damping = 50.0f
val sign = -1.0f val sign = -1.0f

@ -10,8 +10,8 @@
<ImageView <ImageView
android:id="@+id/menu_badge_icon" android:id="@+id/menu_badge_icon"
android:layout_width="20dp" android:layout_width="16dp"
android:layout_height="20dp" android:layout_height="16dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_timer" android:src="@drawable/ic_timer"
android:background="@color/transparent" android:background="@color/transparent"

@ -3,6 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:paddingVertical="@dimen/medium_spacing" android:paddingVertical="@dimen/medium_spacing"
android:paddingHorizontal="@dimen/massive_spacing" android:paddingHorizontal="@dimen/massive_spacing"
@ -12,7 +13,8 @@
android:id="@+id/iconImageView" android:id="@+id/iconImageView"
android:layout_width="12dp" android:layout_width="12dp"
android:layout_height="12dp" android:layout_height="12dp"
android:layout_marginBottom="@dimen/small_spacing" /> android:layout_marginBottom="@dimen/small_spacing"
app:tint="@color/text" />
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"

Loading…
Cancel
Save