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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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>