mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-19 21:58:25 +00:00
Merge pull request #622 from RyanRory/ui
Conversation Screen 2.0 - Search
This commit is contained in:
commit
9b513fa2ba
@ -22,6 +22,8 @@ import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.loader.app.LoaderManager
|
||||
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.link_preview.LinkPreview
|
||||
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.MediaTypes
|
||||
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.RecipientModifiedListener
|
||||
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.messages.VisibleMessageContentViewDelegate
|
||||
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.database.DatabaseFactory
|
||||
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.
|
||||
|
||||
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
|
||||
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
|
||||
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener {
|
||||
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
|
||||
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
|
||||
SearchBottomBar.EventListener {
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private var linkPreviewViewModel: LinkPreviewViewModel? = null
|
||||
private var threadID: Long = -1
|
||||
@ -128,6 +135,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
private var previousText: CharSequence = ""
|
||||
private var currentMentionStartIndex = -1
|
||||
private var isShowingMentionCandidatesView = false
|
||||
// Search
|
||||
var searchViewModel: SearchViewModel? = null
|
||||
var searchViewItem: MenuItem? = null
|
||||
|
||||
private val isScrolledToBottom: Boolean
|
||||
get() {
|
||||
@ -199,6 +209,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
getLatestOpenGroupInfoIfNeeded()
|
||||
setUpBlockedBanner()
|
||||
setUpLinkPreviewObserver()
|
||||
searchBottomBar.setEventListener(this)
|
||||
setUpSearchResultObserver()
|
||||
scrollToFirstUnreadMessageIfNeeded()
|
||||
markAllAsRead()
|
||||
showOrHideInputIfNeeded()
|
||||
@ -371,7 +383,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
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)
|
||||
return true
|
||||
}
|
||||
@ -685,6 +697,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
val actionMode = this.actionMode
|
||||
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
|
||||
actionModeCallback.delegate = this
|
||||
searchViewItem?.collapseActionView()
|
||||
if (actionMode == null) { // Nothing should be selected if this is the case
|
||||
adapter.toggleSelection(message, position)
|
||||
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)
|
||||
}
|
||||
// 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
|
||||
}
|
@ -22,6 +22,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||
private val messageDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
var selectedItems = mutableSetOf<MessageRecord>()
|
||||
private var searchQuery: String? = null
|
||||
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
|
||||
|
||||
sealed class ViewType(val rawValue: Int) {
|
||||
@ -71,7 +72,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
view.snIsSelected = isSelected
|
||||
view.messageTimestampTextView.isVisible = isSelected
|
||||
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.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||
@ -133,4 +134,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun onSearchQueryUpdated(query: String?) {
|
||||
this.searchQuery = query
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
@ -12,17 +12,20 @@ import android.os.AsyncTask
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import kotlinx.android.synthetic.main.activity_conversation_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.avatars.ContactPhoto
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
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.TextSecurePreferences
|
||||
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.toHexString
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
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.util.BitmapUtil
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
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
|
||||
menu.clear()
|
||||
val isOpenGroup = thread.isOpenGroupRecipient
|
||||
@ -92,6 +92,49 @@ object ConversationMenuHelper {
|
||||
} else {
|
||||
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 {
|
||||
@ -121,7 +164,8 @@ object ConversationMenuHelper {
|
||||
}
|
||||
|
||||
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")
|
||||
|
@ -42,7 +42,7 @@ class LinkPreviewView : LinearLayout {
|
||||
// endregion
|
||||
|
||||
// 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()
|
||||
url = linkPreview.url
|
||||
// Thumbnail
|
||||
@ -60,7 +60,7 @@ class LinkPreviewView : LinearLayout {
|
||||
}
|
||||
titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme))
|
||||
// Body
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainLinkPreviewContainer.addView(bodyTextView)
|
||||
// Corner radii
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
|
@ -1,20 +1,20 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
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.loki.utilities.*
|
||||
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
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
@ -58,7 +61,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
|
||||
// region Updating
|
||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
||||
glide: GlideRequests, maxWidth: Int, thread: Recipient) {
|
||||
glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) {
|
||||
// Background
|
||||
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
|
||||
@ -72,7 +75,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
onContentDoubleTap = null
|
||||
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||
val linkPreviewView = LinkPreviewView(context)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
||||
mainContainer.addView(linkPreviewView)
|
||||
onContentClick = { event -> linkPreviewView.calculateHit(event) }
|
||||
// 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,
|
||||
message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, glide)
|
||||
mainContainer.addView(quoteView)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
ViewUtil.setPaddingTop(bodyTextView, 0)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
@ -128,7 +131,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
mainContainer.addView(openGroupInvitationView)
|
||||
onContentClick = { openGroupInvitationView.joinOpenGroup() }
|
||||
} else {
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message)
|
||||
val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery)
|
||||
mainContainer.addView(bodyTextView)
|
||||
onContentClick = { event ->
|
||||
// intersectedModalSpans should only be a list of one item
|
||||
@ -162,7 +165,7 @@ class VisibleMessageContentView : LinearLayout {
|
||||
// region Convenience
|
||||
companion object {
|
||||
|
||||
fun getBodyTextView(context: Context, message: MessageRecord): TextView {
|
||||
fun getBodyTextView(context: Context, message: MessageRecord, searchQuery: String?): TextView {
|
||||
val result = EmojiTextView(context)
|
||||
val vPadding = context.resources.getDimension(R.dimen.small_spacing).toInt()
|
||||
val hPadding = toPx(12, context.resources)
|
||||
@ -186,8 +189,11 @@ class VisibleMessageContentView : LinearLayout {
|
||||
body.removeSpan(urlSpan)
|
||||
body.setSpan(replacementSpan, start, end, flags)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ class VisibleMessageView : LinearLayout {
|
||||
// endregion
|
||||
|
||||
// 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 senderSessionID = sender.address.serialize()
|
||||
val threadID = message.threadId
|
||||
@ -139,7 +139,7 @@ class VisibleMessageView : LinearLayout {
|
||||
var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin
|
||||
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
|
||||
// Populate content view
|
||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread)
|
||||
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery)
|
||||
messageContentView.delegate = contentViewDelegate
|
||||
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -34,6 +34,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/additionalContentContainer"
|
||||
android:layout_width="match_parent"
|
||||
|
70
app/src/main/res/layout/view_search_bottom_bar.xml
Normal file
70
app/src/main/res/layout/view_search_bottom_bar.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user