Merge pull request #622 from RyanRory/ui

Conversation Screen 2.0 - Search
This commit is contained in:
Niels Andriesse 2021-06-30 15:08:46 +10:00 committed by GitHub
commit 9b513fa2ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 385 additions and 24 deletions

View File

@ -22,6 +22,8 @@ import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
@ -53,9 +55,11 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.MediaTypes 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.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.ListenableFuture
@ -74,6 +78,8 @@ import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCa
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.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.SearchViewModel
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
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
@ -108,8 +114,9 @@ import kotlin.math.*
// price we pay is a bit of back and forth between the input bar and the conversation activity. // price we pay is a bit of back and forth between the input bar and the conversation activity.
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener { 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
@ -128,6 +135,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var previousText: CharSequence = "" private var previousText: CharSequence = ""
private var currentMentionStartIndex = -1 private var currentMentionStartIndex = -1
private var isShowingMentionCandidatesView = false private var isShowingMentionCandidatesView = false
// Search
var searchViewModel: SearchViewModel? = null
var searchViewItem: MenuItem? = null
private val isScrolledToBottom: Boolean private val isScrolledToBottom: Boolean
get() { get() {
@ -199,6 +209,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
getLatestOpenGroupInfoIfNeeded() getLatestOpenGroupInfoIfNeeded()
setUpBlockedBanner() setUpBlockedBanner()
setUpLinkPreviewObserver() setUpLinkPreviewObserver()
searchBottomBar.setEventListener(this)
setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded() scrollToFirstUnreadMessageIfNeeded()
markAllAsRead() markAllAsRead()
showOrHideInputIfNeeded() showOrHideInputIfNeeded()
@ -371,7 +383,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) } ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, threadID, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
return true return true
} }
@ -685,6 +697,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val actionMode = this.actionMode val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this actionModeCallback.delegate = this
searchViewItem?.collapseActionView()
if (actionMode == null) { // Nothing should be selected if this is the case if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -1168,4 +1181,46 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
draftDB.insertDrafts(threadID, drafts) draftDB.insertDrafts(threadID, drafts)
} }
// endregion // endregion
}
// region Search
private fun setUpSearchResultObserver() {
val searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
this.searchViewModel = searchViewModel
searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? ->
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() })
}
}
this.searchBottomBar.setData(result.position, result.getResults().size)
})
}
fun onSearchQueryUpdated(query: String?) {
adapter.onSearchQueryUpdated(query)
}
override fun onSearchMoveUpPressed() {
this.searchViewModel?.onMoveUp()
}
override fun onSearchMoveDownPressed() {
this.searchViewModel?.onMoveDown()
}
private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) {
SimpleTask.run(lifecycle, {
DatabaseFactory.getMmsSmsDatabase(this).getMessagePositionInConversation(threadID, timestamp, author)
}) { p: Int -> moveToMessagePosition(p, onMessageNotFound) }
}
private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
if (position >= 0) {
conversationRecyclerView.scrollToPosition(position)
} else {
onMessageNotFound?.run()
}
}
// endregion
}

View File

@ -22,6 +22,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>()
private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
sealed class ViewType(val rawValue: Int) { sealed class ViewType(val rawValue: Int) {
@ -71,7 +72,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.snIsSelected = isSelected view.snIsSelected = isSelected
view.messageTimestampTextView.isVisible = isSelected view.messageTimestampTextView.isVisible = isSelected
val position = viewHolder.adapterPosition val position = viewHolder.adapterPosition
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide) view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) } view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
@ -133,4 +134,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
} }
return null return null
} }
fun onSearchQueryUpdated(query: String?) {
this.searchQuery = query
notifyDataSetChanged()
}
} }

View File

@ -12,17 +12,20 @@ import android.os.AsyncTask
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.avatars.ContactPhoto
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave import org.session.libsession.messaging.sending_receiving.leave
@ -30,11 +33,9 @@ import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.* import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity
@ -43,11 +44,10 @@ import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
object ConversationMenuHelper { object ConversationMenuHelper {
fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) { fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, threadId: Long, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) {
// Prepare // Prepare
menu.clear() menu.clear()
val isOpenGroup = thread.isOpenGroupRecipient val isOpenGroup = thread.isOpenGroupRecipient
@ -92,6 +92,49 @@ object ConversationMenuHelper {
} else { } else {
inflater.inflate(R.menu.menu_conversation_unmuted, menu) inflater.inflate(R.menu.menu_conversation_unmuted, menu)
} }
// Search
val searchViewItem = menu.findItem(R.id.menu_search)
(context as ConversationActivityV2).searchViewItem = searchViewItem
val searchView = searchViewItem.actionView as SearchView
val searchViewModel = context.searchViewModel!!
val queryListener = object : OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(query: String): Boolean {
searchViewModel.onQueryUpdated(query, threadId)
context.searchBottomBar.showLoading()
context.onSearchQueryUpdated(query)
return true
}
}
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(queryListener)
searchViewModel.onSearchOpened()
context.searchBottomBar.visibility = View.VISIBLE
context.searchBottomBar.setData(0, 0)
context.inputBar.visibility = View.GONE
for (i in 0 until menu.size()) {
if (menu.getItem(i) != searchViewItem) {
menu.getItem(i).isVisible = false
}
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(null)
searchViewModel.onSearchClosed()
context.searchBottomBar.visibility = View.GONE
context.inputBar.visibility = View.VISIBLE
context.onSearchQueryUpdated(null)
context.invalidateOptionsMenu()
return true
}
})
} }
fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean { fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean {
@ -121,7 +164,8 @@ object ConversationMenuHelper {
} }
private fun search(context: Context) { private fun search(context: Context) {
Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show() // TODO: Implement val searchViewModel = (context as ConversationActivityV2).searchViewModel!!
searchViewModel.onSearchOpened()
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")

View File

@ -42,7 +42,7 @@ class LinkPreviewView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, glide: GlideRequests, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, searchQuery: String?) {
val linkPreview = message.linkPreviews.first() val linkPreview = message.linkPreviews.first()
url = linkPreview.url url = linkPreview.url
// Thumbnail // Thumbnail
@ -60,7 +60,7 @@ class LinkPreviewView : LinearLayout {
} }
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
// Body // Body
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainLinkPreviewContainer.addView(bodyTextView) mainLinkPreviewContainer.addView(bodyTextView)
// Corner radii // Corner radii
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)

View File

@ -1,20 +1,20 @@
package org.thoughtcrime.securesms.conversation.v2.messages package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.ReplacementSpan
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -39,6 +39,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
@ -58,7 +61,7 @@ class VisibleMessageContentView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
glide: GlideRequests, maxWidth: Int, thread: Recipient) { glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) {
// Background // Background
val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster) val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster)
val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color
@ -72,7 +75,7 @@ class VisibleMessageContentView : LinearLayout {
onContentDoubleTap = null onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context) val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView) mainContainer.addView(linkPreviewView)
onContentClick = { event -> linkPreviewView.calculateHit(event) } onContentClick = { event -> linkPreviewView.calculateHit(event) }
// Body text view is inside the link preview for layout convenience // Body text view is inside the link preview for layout convenience
@ -86,7 +89,7 @@ class VisibleMessageContentView : LinearLayout {
quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread,
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide) message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
mainContainer.addView(quoteView) mainContainer.addView(quoteView)
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
ViewUtil.setPaddingTop(bodyTextView, 0) ViewUtil.setPaddingTop(bodyTextView, 0)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { event -> onContentClick = { event ->
@ -128,7 +131,7 @@ class VisibleMessageContentView : LinearLayout {
mainContainer.addView(openGroupInvitationView) mainContainer.addView(openGroupInvitationView)
onContentClick = { openGroupInvitationView.joinOpenGroup() } onContentClick = { openGroupInvitationView.joinOpenGroup() }
} else { } else {
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message) val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
mainContainer.addView(bodyTextView) mainContainer.addView(bodyTextView)
onContentClick = { event -> onContentClick = { event ->
// intersectedModalSpans should only be a list of one item // intersectedModalSpans should only be a list of one item
@ -162,7 +165,7 @@ class VisibleMessageContentView : LinearLayout {
// region Convenience // region Convenience
companion object { companion object {
fun getBodyTextView(context: Context, message: MessageRecord): TextView { fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
val result = EmojiTextView(context) val result = EmojiTextView(context)
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt() val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
val hPadding = toPx(12, context.resources) val hPadding = toPx(12, context.resources)
@ -186,8 +189,11 @@ class VisibleMessageContentView : LinearLayout {
body.removeSpan(urlSpan) body.removeSpan(urlSpan)
body.setSpan(replacementSpan, start, end, flags) 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 result.text = body
return result return result
} }

View File

@ -71,7 +71,7 @@ class VisibleMessageView : LinearLayout {
// endregion // endregion
// region Updating // region Updating
fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests) { fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, glide: GlideRequests, searchQuery: String?) {
val sender = message.individualRecipient val sender = message.individualRecipient
val senderSessionID = sender.address.serialize() val senderSessionID = sender.address.serialize()
val threadID = message.threadId val threadID = message.threadId
@ -139,7 +139,7 @@ class VisibleMessageView : LinearLayout {
var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin
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, searchQuery)
messageContentView.delegate = contentViewDelegate messageContentView.delegate = contentViewDelegate
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
} }

View File

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
import network.loki.messenger.R
class SearchBottomBar : LinearLayout {
private var eventListener: EventListener? = null
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
}
fun setData(position: Int, count: Int) {
searchProgressWheel.visibility = GONE
searchUp.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveUpPressed()
}
}
searchDown.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveDownPressed()
}
}
if (count > 0) {
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
} else {
searchPosition.text = ""
}
setViewEnabled(searchUp, position < count - 1)
setViewEnabled(searchDown, position > 0)
}
fun showLoading() {
searchProgressWheel.visibility = VISIBLE
}
private fun setViewEnabled(view: View, enabled: Boolean) {
view.isEnabled = enabled
view.alpha = if (enabled) 1f else 0.25f
}
fun setEventListener(eventListener: EventListener?) {
this.eventListener = eventListener
}
interface EventListener {
fun onSearchMoveUpPressed()
fun onSearchMoveDownPressed()
}
}

View File

@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import org.session.libsession.utilities.Debouncer
import org.session.libsession.utilities.Util.runOnMain
import org.session.libsession.utilities.concurrent.SignalExecutors
import org.thoughtcrime.securesms.contacts.ContactAccessor
import org.thoughtcrime.securesms.database.CursorList
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.util.CloseableLiveData
import java.io.Closeable
class SearchViewModel(application: Application) : AndroidViewModel(application) {
private val searchRepository: SearchRepository
private val result: CloseableLiveData<SearchResult>
private val debouncer: Debouncer
private var firstSearch = false
private var searchOpen = false
private var activeQuery: String? = null
private var activeThreadId: Long = 0
val searchResults: LiveData<SearchResult>
get() = result
fun onQueryUpdated(query: String, threadId: Long) {
if (query == activeQuery) {
return
}
updateQuery(query, threadId)
}
fun onMissingResult() {
if (activeQuery != null) {
updateQuery(activeQuery!!, activeThreadId)
}
}
fun onMoveUp() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.min(result.value!!.position + 1, messages.size - 1)
result.setValue(SearchResult(messages, position), false)
}
fun onMoveDown() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.max(result.value!!.position - 1, 0)
result.setValue(SearchResult(messages, position), false)
}
fun onSearchOpened() {
searchOpen = true
firstSearch = true
}
fun onSearchClosed() {
searchOpen = false
activeQuery = null
debouncer.clear()
result.close()
}
override fun onCleared() {
super.onCleared()
result.close()
}
private fun updateQuery(query: String, threadId: Long) {
activeQuery = query
activeThreadId = threadId
debouncer.publish {
firstSearch = false
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
runOnMain {
if (searchOpen && query == activeQuery) {
result.setValue(SearchResult(messages, 0))
} else {
messages.close()
}
}
}
}
}
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
fun getResults(): List<MessageResult?> {
return results
}
override fun close() {
results.close()
}
}
init {
val context = application.applicationContext
result = CloseableLiveData()
debouncer = Debouncer(500)
searchRepository = SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getThreadDatabase(context),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL)
}
}

View File

@ -34,6 +34,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
<org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
android:id="@+id/searchBottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"/>
<FrameLayout <FrameLayout
android:id="@+id/additionalContentContainer" android:id="@+id/additionalContentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/searchBottomBarConstraintLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/input_bar_height"
android:background="@color/compose_view_background"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical">
<ImageView
android:id="@+id/searchUp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:padding="4dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_up_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
<ImageView
android:id="@+id/searchDown"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="4dp"
android:layout_gravity="center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_down_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
</LinearLayout>
<TextView
android:id="@+id/searchPosition"
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="37 of 73"
android:textStyle="bold"/>
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.DoubleBounce"
android:id="@+id/searchProgressWheel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:background="@color/compose_view_background"
app:SpinKit_Color="?android:textColorPrimary"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>