This commit is contained in:
ryanzhao 2021-06-30 11:48:54 +10:00
commit ba1099d276
9 changed files with 101 additions and 18 deletions

View File

@ -61,6 +61,7 @@ import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
@ -75,6 +76,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.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
@ -113,7 +115,8 @@ import kotlin.math.*
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, SearchBottomBar.EventListener { ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
SearchBottomBar.EventListener {
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
@ -136,6 +139,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
var searchViewModel: SearchViewModel? = null var searchViewModel: SearchViewModel? = null
var searchViewItem: MenuItem? = null var searchViewItem: MenuItem? = null
private val isScrolledToBottom: Boolean
get() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
return position == 0
}
private val layoutManager: LinearLayoutManager private val layoutManager: LinearLayoutManager
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager } get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
@ -155,6 +164,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}, },
glide glide
) )
adapter.visibleMessageContentViewDelegate = this
adapter adapter
} }
@ -194,6 +204,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID) unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID)
updateUnreadCountIndicator() updateUnreadCountIndicator()
setUpTypingObserver() setUpTypingObserver()
setUpRecipientObserver()
updateSubtitle() updateSubtitle()
getLatestOpenGroupInfoIfNeeded() getLatestOpenGroupInfoIfNeeded()
setUpBlockedBanner() setUpBlockedBanner()
@ -309,7 +320,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun setUpTypingObserver() { private fun setUpTypingObserver() {
ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state -> ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state ->
val recipients = if (state != null) state.typists else listOf() val recipients = if (state != null) state.typists else listOf()
typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() // FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom
typingIndicatorViewContainer.setTypists(recipients) typingIndicatorViewContainer.setTypists(recipients)
inputBarHeightChanged(inputBar.height) inputBarHeightChanged(inputBar.height)
} }
@ -323,6 +336,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
private fun setUpRecipientObserver() {
thread.addListener(this)
}
private fun getLatestOpenGroupInfoIfNeeded() { private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
@ -376,7 +393,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// endregion // endregion
// region Updating & Animation override fun onModified(recipient: Recipient) {
if (thread.isContactRecipient) {
blockedBanner.isVisible = thread.isBlocked
}
updateSubtitle()
}
private fun markAllAsRead() { private fun markAllAsRead() {
val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true) val messages = DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
if (thread.isGroupRecipient) { if (thread.isGroupRecipient) {
@ -577,8 +600,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun handleRecyclerViewScrolled() { private fun handleRecyclerViewScrolled() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition() val alpha = if (!isScrolledToBottom) 1.0f else 0.0f
val alpha = if (position > 0) 1.0f else 0.0f // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
val wasTypingIndicatorVisibleBefore = typingIndicatorViewContainer.isVisible
typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
val isTypingIndicatorVisibleAfter = typingIndicatorViewContainer.isVisible
if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) {
inputBarHeightChanged(inputBar.height)
}
scrollToBottomButton.alpha = alpha scrollToBottomButton.alpha = alpha
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition()) unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
updateUnreadCountIndicator() updateUnreadCountIndicator()
@ -748,6 +778,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")
@ -916,10 +951,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() {

View File

@ -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
@ -21,6 +22,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context) private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
var selectedItems = mutableSetOf<MessageRecord>() var selectedItems = mutableSetOf<MessageRecord>()
private var searchQuery: String? = null private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
sealed class ViewType(val rawValue: Int) { sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0) object Visible : ViewType(0)
@ -73,6 +75,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)
} }
@ -114,8 +117,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
} }

View File

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

View File

@ -318,13 +318,11 @@ object ConversationMenuHelper {
} }
private fun unmute(context: Context, thread: Recipient) { private fun unmute(context: Context, thread: Recipient) {
thread.setMuted(0)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0) DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0)
} }
private fun mute(context: Context, thread: Recipient) { private fun mute(context: Context, thread: Recipient) {
MuteDialog.show(context) { until: Long -> MuteDialog.show(context) { until: Long ->
thread.setMuted(until)
DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until) DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
} }
} }

View File

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

View File

@ -22,6 +22,8 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat 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 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
@ -43,6 +45,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() }
@ -89,6 +92,13 @@ class VisibleMessageContentView : LinearLayout {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
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)
@ -192,4 +202,9 @@ class VisibleMessageContentView : LinearLayout {
} }
} }
// endregion // endregion
}
interface VisibleMessageContentViewDelegate {
fun scrollToMessageIfPossible(timestamp: Long)
} }

View File

@ -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, searchQuery) messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
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

View File

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

View File

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