Add the ability to jump to a specific date in search.
This commit is contained in:
parent
d5bf16b91a
commit
7447ed2eac
9 changed files with 283 additions and 23 deletions
|
@ -22,6 +22,7 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
|
|||
private View searchUp;
|
||||
private TextView searchPositionText;
|
||||
private View progressWheel;
|
||||
private View jumpToDateButton;
|
||||
|
||||
private EventListener eventListener;
|
||||
|
||||
|
@ -42,6 +43,7 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
|
|||
this.searchDown = findViewById(R.id.conversation_search_down);
|
||||
this.searchPositionText = findViewById(R.id.conversation_search_position);
|
||||
this.progressWheel = findViewById(R.id.conversation_search_progress_wheel);
|
||||
this.jumpToDateButton = findViewById(R.id.conversation_jump_to_date_button);
|
||||
}
|
||||
|
||||
public void setData(int position, int count) {
|
||||
|
@ -65,6 +67,12 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
|
|||
searchPositionText.setText(R.string.ConversationActivity_no_results);
|
||||
}
|
||||
|
||||
jumpToDateButton.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onDatePickerSelected();
|
||||
}
|
||||
});
|
||||
|
||||
setViewEnabled(searchUp, position < (count - 1));
|
||||
setViewEnabled(searchDown, position > 0);
|
||||
}
|
||||
|
@ -85,5 +93,6 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
|
|||
public interface EventListener {
|
||||
void onSearchMoveUpPressed();
|
||||
void onSearchMoveDownPressed();
|
||||
void onDatePickerSelected();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ import androidx.recyclerview.widget.ConversationLayoutManager
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar.Duration
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -305,6 +307,8 @@ import org.thoughtcrime.securesms.util.StorageUtil
|
|||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.atMidnight
|
||||
import org.thoughtcrime.securesms.util.atUTC
|
||||
import org.thoughtcrime.securesms.util.createActivityViewModel
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
|
@ -314,12 +318,14 @@ import org.thoughtcrime.securesms.util.hasGiftBadge
|
|||
import org.thoughtcrime.securesms.util.hasNonTextSlide
|
||||
import org.thoughtcrime.securesms.util.isValidReactionTarget
|
||||
import org.thoughtcrime.securesms.util.savedStateViewModel
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
@ -1616,7 +1622,7 @@ class ConversationFragment :
|
|||
if (result.results.isNotEmpty()) {
|
||||
val messageResult = result.results[result.position]
|
||||
disposables += viewModel
|
||||
.moveToSearchResult(messageResult)
|
||||
.moveToDate(messageResult.receivedTimestampMs)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
moveToPosition(it)
|
||||
|
@ -2457,6 +2463,16 @@ class ConversationFragment :
|
|||
return isScrolledToBottom() || layoutManager.findFirstVisibleItemPosition() <= 0
|
||||
}
|
||||
|
||||
private fun closeChatSearch() {
|
||||
isSearchRequested = false
|
||||
searchViewModel.onSearchClosed()
|
||||
searchNav.visible = false
|
||||
inputPanel.setHideForSearch(false)
|
||||
viewModel.setSearchQuery(null)
|
||||
binding.conversationDisabledInput.visible = true
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls animation and visibility of the scrollDateHeader.
|
||||
*/
|
||||
|
@ -3199,6 +3215,7 @@ class ConversationFragment :
|
|||
searchNav.visible = true
|
||||
searchNav.setData(0, 0)
|
||||
inputPanel.setHideForSearch(true)
|
||||
viewModel.onChatSearchOpened()
|
||||
binding.conversationDisabledInput.visible = false
|
||||
|
||||
(0 until menu.size()).forEach {
|
||||
|
@ -3212,13 +3229,7 @@ class ConversationFragment :
|
|||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchView.setOnQueryTextListener(null)
|
||||
isSearchRequested = false
|
||||
searchViewModel.onSearchClosed()
|
||||
searchNav.visible = false
|
||||
inputPanel.setHideForSearch(false)
|
||||
binding.conversationDisabledInput.visible = true
|
||||
viewModel.setSearchQuery(null)
|
||||
invalidateOptionsMenu()
|
||||
closeChatSearch()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
@ -4153,6 +4164,47 @@ class ConversationFragment :
|
|||
override fun onSearchMoveDownPressed() {
|
||||
searchViewModel.onMoveDown()
|
||||
}
|
||||
|
||||
override fun onDatePickerSelected() {
|
||||
disposables += viewModel.getEarliestMessageDate().subscribe { earliestDate ->
|
||||
val local = LocalDateTime.now()
|
||||
.atMidnight()
|
||||
.atUTC()
|
||||
.toMillis()
|
||||
val datePicker =
|
||||
MaterialDatePicker.Builder
|
||||
.datePicker()
|
||||
.setTitleText(getString(R.string.ScheduleMessageTimePickerBottomSheet__select_date_title))
|
||||
.setSelection(local)
|
||||
.setCalendarConstraints(
|
||||
CalendarConstraints.Builder()
|
||||
.setValidator(viewModel.jumpToDateValidator)
|
||||
.setStart(earliestDate)
|
||||
.setEnd(local)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
datePicker.addOnDismissListener {
|
||||
datePicker.clearOnDismissListeners()
|
||||
datePicker.clearOnPositiveButtonClickListeners()
|
||||
}
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener { selectedDate ->
|
||||
if (selectedDate != null) {
|
||||
disposables += viewModel
|
||||
.moveToDate(selectedDate)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy { position ->
|
||||
moveToPosition(position - 1)
|
||||
closeChatSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
datePicker.show(childFragmentManager, "DATE_PICKER")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ToolbarDependentMarginListener(private val toolbar: Toolbar) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
|
|
@ -86,7 +86,6 @@ import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
|||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.search.MessageResult
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
|
@ -265,9 +264,9 @@ class ConversationRepository(
|
|||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun getMessageResultPosition(threadId: Long, messageResult: MessageResult): Single<Int> {
|
||||
fun getMessageResultPosition(threadId: Long, receivedTimestamp: Long): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.messages.getMessagePositionInConversation(threadId, messageResult.receivedTimestampMs)
|
||||
SignalDatabase.messages.getMessagePositionInConversation(threadId, receivedTimestamp)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
@ -580,6 +579,12 @@ class ConversationRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fun getEarliestMessageDate(threadId: Long): Single<Long> {
|
||||
return Single
|
||||
.fromCallable { SignalDatabase.messages.getEarliestMessageDate(threadId) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Glide target for a contact photo which expects an error drawable, and publishes
|
||||
* the result to the given emitter.
|
||||
|
|
|
@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.mms.Slide
|
|||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.search.MessageResult
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
|
@ -158,6 +157,10 @@ class ConversationViewModel(
|
|||
|
||||
private val startExpiration = BehaviorSubject.create<MessageTable.ExpirationInfo>()
|
||||
|
||||
private val _jumpToDateValidator: JumpToDateValidator by lazy { JumpToDateValidator(threadId) }
|
||||
val jumpToDateValidator: JumpToDateValidator
|
||||
get() = _jumpToDateValidator
|
||||
|
||||
init {
|
||||
disposables += recipient
|
||||
.subscribeBy {
|
||||
|
@ -312,8 +315,8 @@ class ConversationViewModel(
|
|||
return repository.getQuotedMessagePosition(threadId, quote)
|
||||
}
|
||||
|
||||
fun moveToSearchResult(messageResult: MessageResult): Single<Int> {
|
||||
return repository.getMessageResultPosition(threadId, messageResult)
|
||||
fun moveToDate(receivedTimestamp: Long): Single<Int> {
|
||||
return repository.getMessageResultPosition(threadId, receivedTimestamp)
|
||||
}
|
||||
|
||||
fun getNextMentionPosition(): Single<Int> {
|
||||
|
@ -507,4 +510,15 @@ class ConversationViewModel(
|
|||
fun markLastSeen() {
|
||||
repository.markLastSeen(threadId)
|
||||
}
|
||||
|
||||
fun onChatSearchOpened() {
|
||||
// Trigger the lazy load, so we can race initialization of the validator
|
||||
_jumpToDateValidator
|
||||
}
|
||||
|
||||
fun getEarliestMessageDate(): Single<Long> {
|
||||
return repository
|
||||
.getEarliestMessageDate(threadId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logTime
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.LRUCache
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.concurrent.locks.Condition
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* A calendar validator for jumping to a specific date in a conversation.
|
||||
* This is used to prevent the user from jumping to a date where there are no messages.
|
||||
*
|
||||
* [isValid] is called on the main thread, so we try to race it and fetch the data ahead of time, fetching data in bulk and caching it.
|
||||
*/
|
||||
@Parcelize
|
||||
class JumpToDateValidator(val threadId: Long) : DateValidator {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(JumpToDateValidator::class.java)
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val condition: Condition = lock.newCondition()
|
||||
|
||||
@IgnoredOnParcel
|
||||
private val cachedDates: MutableMap<Long, LookupState> = LRUCache(500)
|
||||
|
||||
init {
|
||||
val startOfDay = LocalDateTime.now(ZoneOffset.UTC).withHour(0).withMinute(0).withSecond(0).withNano(0).toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||
loadAround(startOfDay, allowPrefetch = true)
|
||||
}
|
||||
|
||||
override fun isValid(dateStart: Long): Boolean {
|
||||
return lock.withLock {
|
||||
var value = cachedDates[dateStart]
|
||||
|
||||
while (value == null || value == LookupState.PENDING) {
|
||||
loadAround(dateStart, allowPrefetch = true)
|
||||
condition.await()
|
||||
value = cachedDates[dateStart]
|
||||
}
|
||||
|
||||
cachedDates[dateStart] == LookupState.FOUND
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a date, this will load all of the dates for entire month the date is in.
|
||||
*/
|
||||
private fun loadAround(dateStart: Long, allowPrefetch: Boolean) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val startOfDay = LocalDateTime.ofInstant(Instant.ofEpochMilli(dateStart), ZoneOffset.UTC)
|
||||
|
||||
val startOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.firstDayOfMonth())
|
||||
.withHour(0).withMinute(0).withSecond(0).withNano(0)
|
||||
.toInstant(ZoneOffset.UTC)
|
||||
.toEpochMilli()
|
||||
|
||||
val endOfMonth = startOfDay
|
||||
.with(TemporalAdjusters.lastDayOfMonth())
|
||||
.withHour(0).withMinute(0).withSecond(0).withNano(0)
|
||||
.toInstant(ZoneOffset.UTC)
|
||||
.toEpochMilli()
|
||||
|
||||
val daysOfMonth = (startOfMonth..endOfMonth step 1.days.inWholeMilliseconds).toSet() + dateStart
|
||||
|
||||
val lookupsNeeded = lock.withLock {
|
||||
daysOfMonth
|
||||
.filter { !cachedDates.containsKey(it) }
|
||||
.onEach { cachedDates[it] = LookupState.PENDING }
|
||||
}
|
||||
|
||||
if (lookupsNeeded.isEmpty()) {
|
||||
return@execute
|
||||
}
|
||||
|
||||
val existence = logTime(TAG, "query(${lookupsNeeded.size})", decimalPlaces = 2) {
|
||||
SignalDatabase.messages.messageExistsOnDays(threadId, lookupsNeeded)
|
||||
}
|
||||
|
||||
lock.withLock {
|
||||
cachedDates.putAll(existence.mapValues { if (it.value) LookupState.FOUND else LookupState.NOT_FOUND })
|
||||
|
||||
if (allowPrefetch) {
|
||||
val dayInPreviousMonth = startOfMonth - 1.days.inWholeMilliseconds
|
||||
if (!cachedDates.containsKey(dayInPreviousMonth)) {
|
||||
loadAround(dayInPreviousMonth, allowPrefetch = false)
|
||||
}
|
||||
|
||||
val dayInNextMonth = endOfMonth + 1.days.inWholeMilliseconds
|
||||
if (!cachedDates.containsKey(dayInNextMonth)) {
|
||||
loadAround(dayInNextMonth, allowPrefetch = false)
|
||||
}
|
||||
}
|
||||
|
||||
condition.signalAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class LookupState {
|
||||
FOUND,
|
||||
NOT_FOUND,
|
||||
PENDING
|
||||
}
|
||||
}
|
|
@ -3959,6 +3959,29 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
return getMessagePositionInConversation(threadId, 0, receivedTimestamp)
|
||||
}
|
||||
|
||||
fun messageExistsOnDays(threadId: Long, dayStarts: Collection<Long>): Map<Long, Boolean> {
|
||||
if (dayStarts.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
return dayStarts.associateWith { startOfDay ->
|
||||
readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $DATE_RECEIVED >= $startOfDay AND $DATE_RECEIVED < $startOfDay + 86400000 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0")
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
fun getEarliestMessageDate(threadId: Long): Long {
|
||||
return readableDatabase
|
||||
.select(DATE_RECEIVED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0")
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.readToSingleLong(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the position of the message with the provided timestamp in the query results you'd
|
||||
* get from calling [.getConversation].
|
||||
|
@ -3970,22 +3993,16 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
* @param groupStoryId Ignored if passed value is <= 0
|
||||
*/
|
||||
fun getMessagePositionInConversation(threadId: Long, groupStoryId: Long, receivedTimestamp: Long): Int {
|
||||
val order: String
|
||||
val selection: String
|
||||
|
||||
if (groupStoryId > 0) {
|
||||
order = "$DATE_RECEIVED ASC"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
val selection = if (groupStoryId > 0) {
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
} else {
|
||||
order = "$DATE_RECEIVED DESC"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
"$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where(selection)
|
||||
.orderBy(order)
|
||||
.run()
|
||||
.readToSingleInt(-1)
|
||||
}
|
||||
|
|
12
app/src/main/res/drawable/symbol_calendar_search_24.xml
Normal file
12
app/src/main/res/drawable/symbol_calendar_search_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.5 0.75c0.55 0 1 0.45 1 1v1.88h7V1.75c0-0.55 0.45-1 1-1s1 0.45 1 1v1.88c0.36 0 0.68 0 0.96 0.03 0.47 0.04 0.91 0.12 1.32 0.33 0.64 0.33 1.15 0.84 1.48 1.48 0.2 0.4 0.29 0.85 0.33 1.32 0.04 0.45 0.04 1 0.04 1.67v2.04c0 0.48-0.4 0.88-0.88 0.88s-0.88-0.4-0.88-0.88V9.62H3.13V17c0 0.71 0 1.2 0.04 1.57 0.03 0.36 0.08 0.54 0.14 0.67 0.16 0.3 0.4 0.55 0.71 0.7 0.13 0.07 0.3 0.12 0.67 0.15 0.37 0.03 0.86 0.04 1.57 0.04H11c0.48 0 0.88 0.39 0.88 0.87s-0.4 0.88-0.88 0.88H6.21c-0.67 0-1.22 0-1.67-0.04-0.47-0.04-0.91-0.12-1.32-0.33-0.64-0.33-1.15-0.84-1.48-1.48-0.2-0.4-0.29-0.85-0.33-1.32-0.04-0.45-0.04-1-0.03-1.67V8.46c0-0.67 0-1.22 0.03-1.67 0.04-0.47 0.12-0.91 0.33-1.32 0.33-0.64 0.84-1.15 1.48-1.48 0.4-0.2 0.85-0.29 1.32-0.33 0.28-0.02 0.6-0.03 0.96-0.03V1.75c0-0.55 0.45-1 1-1Zm12.37 7.13c0-0.4 0-0.7-0.03-0.95-0.03-0.36-0.08-0.54-0.14-0.67-0.16-0.3-0.4-0.55-0.71-0.7-0.13-0.07-0.3-0.12-0.67-0.15-0.37-0.03-0.86-0.04-1.57-0.04h-9.5c-0.71 0-1.2 0-1.57 0.04C4.32 5.44 4.14 5.49 4 5.55c-0.3 0.16-0.55 0.4-0.7 0.71-0.07 0.13-0.12 0.3-0.15 0.67-0.02 0.25-0.03 0.55-0.03 0.95h15.74Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.38 17c0-2.55 2.07-4.63 4.62-4.63 2.55 0 4.63 2.08 4.63 4.63 0 0.96-0.3 1.85-0.8 2.6l2.54 2.53c0.34 0.34 0.34 0.9 0 1.24-0.34 0.34-0.9 0.34-1.24 0l-2.54-2.54c-0.74 0.5-1.63 0.8-2.59 0.8-2.55 0-4.63-2.08-4.63-4.63ZM17 14.12c-1.59 0-2.88 1.3-2.88 2.88 0 1.59 1.3 2.88 2.88 2.88 1.59 0 2.88-1.3 2.88-2.88 0-1.59-1.3-2.88-2.88-2.88Z"/>
|
||||
</vector>
|
|
@ -11,6 +11,19 @@
|
|||
tools:visibility="visible"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/conversation_jump_to_date_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/symbol_calendar_search_24"
|
||||
android:tint="@color/signal_colorOnSurface"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:layout_marginStart="16dp"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/conversation_search_position"
|
||||
style="@style/Signal.Text.Body"
|
||||
|
|
|
@ -8,6 +8,8 @@ package org.signal.core.util
|
|||
import org.signal.core.util.logging.Log
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
/**
|
||||
* Simple utility to easily track the time a multi-step operation takes via splits.
|
||||
|
@ -77,3 +79,13 @@ class Stopwatch @JvmOverloads constructor(private val title: String, private val
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs how long it takes to perform the operation.
|
||||
*/
|
||||
@OptIn(ExperimentalTime::class)
|
||||
inline fun <T> logTime(tag: String, label: String, decimalPlaces: Int = 0, block: () -> T): T {
|
||||
val result = measureTimedValue(block)
|
||||
Log.d(tag, "$label: ${result.duration.toDouble(DurationUnit.MILLISECONDS).roundedString(decimalPlaces)}")
|
||||
return result.value
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue