Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ configurations.configureEach {
exclude(module = "commons-logging")
}

val canonicalVersionCode = 448
val canonicalVersionName = "1.33.2"
val canonicalVersionCode = 449
val canonicalVersionName = "1.33.3"

val postFixSize = 10
val abiPostFix = mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package org.session.libsession.network.onion

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -15,7 +17,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withTimeoutOrNull
import org.session.libsession.network.model.Path
import org.session.libsession.network.model.PathStatus
Expand Down Expand Up @@ -59,9 +61,7 @@ open class PathManager @Inject constructor(
private val pathSize: Int = 3
private val targetPathCount: Int = 2

private val _paths = MutableStateFlow(
sanitizePaths(storage.getOnionRequestPaths())
)
private val _paths = MutableStateFlow<List<Path>>(emptyList())
val paths: StateFlow<List<Path>> = _paths.asStateFlow()

// Used for synchronization
Expand Down Expand Up @@ -91,17 +91,22 @@ open class PathManager @Inject constructor(
if (_paths.value.isEmpty()) PathStatus.ERROR else PathStatus.READY
)

// Warm up from persisted paths without blocking construction.
// Stored as a Deferred so getPath() can await it for deterministic completion.
private val warmUpJob: Deferred<Unit> = scope.async {
val persisted = sanitizePaths(storage.getOnionRequestPaths())
_paths.update { current -> if (current.isEmpty()) persisted else current }
}

init {
// persist to DB whenever paths change
scope.launch {
_paths.drop(1).collectLatest { paths ->
if (paths.isEmpty()) storage.clearOnionRequestPaths()
else {
try {
storage.setOnionRequestPaths(paths)
} catch (e: Exception) {
Log.e("Onion Request", "Failed to persist paths to storage, keeping in-memory only", e)
}
try {
if (paths.isEmpty()) storage.clearOnionRequestPaths()
else storage.setOnionRequestPaths(paths)
} catch (e: Exception) {
Log.e("Onion Request", "Failed to persist paths to storage, keeping in-memory only", e)
}
}
}
Expand All @@ -112,6 +117,9 @@ open class PathManager @Inject constructor(
// -----------------------------

suspend fun getPath(exclude: Snode? = null): Path {
// Ensure persisted paths are loaded before checking. No-op after first completion.
warmUpJob.await()

directory.refreshPoolIfStaleAsync()
rotatePathsIfStale()

Expand Down Expand Up @@ -374,7 +382,6 @@ open class PathManager @Inject constructor(
}

_paths.value = storage.getOnionRequestPaths()

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,25 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
}
}
}

// bottom search bar
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.searchOpened.collectLatest { isSearchOpen ->

if (isSearchOpen) {
binding.searchBottomBar.visibility = View.VISIBLE
} else {
binding.searchBottomBar.visibility = View.GONE

adapter.onSearchQueryUpdated(null)
invalidateOptionsMenu()
}

binding.root.requestApplyInsets()
}
}
}
}

private fun scrollToFirstUnreadMessageOrBottom() {
Expand Down Expand Up @@ -3095,36 +3114,44 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,

// region Search
private fun setUpSearchResultObserver() {
searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? ->
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
if(!gotoMessageById(it.messageId, smoothScroll = false, highlight = true)) {
searchViewModel.onMissingResult()
searchViewModel.searchResults.observe(this) { result ->
if (result == null) return@observe

val query = searchViewModel.searchQuery.value

try {
val results = result.getResults()
val size = results.size
val position = result.position

if (position in 0 until size) {
results[position]?.let { message ->
if (!gotoMessageById(message.messageId, smoothScroll = false, highlight = true)) {
searchViewModel.onMissingResult()
}
}
}
}

binding.searchBottomBar.setData(result.position, result.getResults().size, searchViewModel.searchQuery.value)
})
binding.searchBottomBar.setData(position, size, query)
} catch (e: android.database.StaleDataException) {
binding.searchBottomBar.setData(0, 0, query)
}
}
}

fun onSearchOpened() {
viewModel.onSearchOpened()
searchViewModel.onSearchOpened()
binding.searchBottomBar.visibility = View.VISIBLE
binding.searchBottomBar.setData(0, 0, searchViewModel.searchQuery.value)
binding.root.requestApplyInsets()

binding.searchBottomBar.setData(
0,
0,
searchViewModel.searchQuery.value
)
}

fun onSearchClosed() {
viewModel.onSearchClosed()
searchViewModel.onSearchClosed()
binding.searchBottomBar.visibility = View.GONE
binding.root.requestApplyInsets()
adapter.onSearchQueryUpdated(null)
invalidateOptionsMenu()
}

fun onSearchQueryUpdated(query: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class ConversationViewModel @AssistedInject constructor(
}

private val _searchOpened = MutableStateFlow(false)
val searchOpened : StateFlow<Boolean> get() = _searchOpened

val appBarData: StateFlow<ConversationAppBarData> = combine(
recipientFlow.repeatedWithEffectiveNotifyTypeChange(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,18 @@ class SearchBottomBar : LinearLayout {

fun initialize() {
binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true)

binding.searchUp.setOnClickListener {
eventListener?.onSearchMoveUpPressed()
}

binding.searchDown.setOnClickListener {
eventListener?.onSearchMoveDownPressed()
}
}

fun setData(position: Int, count: Int, searchQuery: String?) = with(binding) {
binding.loading.visibility = GONE
searchUp.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveUpPressed()
}
}
searchDown.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveDownPressed()
}
}
if (count > 0) { // we have results
searchPosition.text = resources.getQuantityString(R.plurals.searchMatches, count, position + 1, count)
} else if ( // we have a legitimate query but no results
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SearchViewModel @Inject constructor(
private val debouncer: Debouncer = Debouncer(200)
private var searchOpen = false
private var activeThreadId: Long = 0
private var currentPosition: Int = 0
val searchResults: LiveData<SearchResult>
get() = result

Expand All @@ -42,21 +43,25 @@ class SearchViewModel @Inject constructor(

fun onMissingResult() {
if (mutableSearchQuery.value != null) {
updateQuery(mutableSearchQuery.value!!, activeThreadId)
updateQuery(mutableSearchQuery.value!!, activeThreadId, currentPosition)
}
}

fun onMoveUp() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.min(result.value!!.position + 1, messages.size - 1)
val currentResult = result.value ?: return
val messages = currentResult.getResults() as CursorList<MessageResult?>
val position = minOf(currentResult.position + 1, messages.size - 1)
currentPosition = position
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)
val currentResult = result.value ?: return
val messages = currentResult.getResults() as CursorList<MessageResult?>
val position = maxOf(currentResult.position - 1, 0)
currentPosition = position
result.setValue(SearchResult(messages, position), false)
}

Expand All @@ -67,6 +72,7 @@ class SearchViewModel @Inject constructor(
fun onSearchClosed() {
searchOpen = false
mutableSearchQuery.value = null
currentPosition = 0
debouncer.clear()
result.close()
}
Expand All @@ -76,11 +82,12 @@ class SearchViewModel @Inject constructor(
result.close()
}

private fun updateQuery(query: String, threadId: Long) {
private fun updateQuery(query: String, threadId: Long, requestedPosition: Int = currentPosition) {
mutableSearchQuery.value = query
activeThreadId = threadId

if(query.length < MIN_QUERY_SIZE) {
currentPosition = 0
result.value = SearchResult(CursorList.emptyList(), 0)
return
}
Expand All @@ -89,7 +96,13 @@ class SearchViewModel @Inject constructor(
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
runOnMain {
if (searchOpen && query == mutableSearchQuery.value) {
result.setValue(SearchResult(messages, 0))
val position = if (messages.isEmpty()) {
0
} else {
requestedPosition.coerceIn(0, messages.size - 1)
}
currentPosition = position
result.setValue(SearchResult(messages, position))
} else {
messages.close()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static byte[] unseal(@NonNull SealedData sealedData) {
}
}

private static SecretKey getOrCreateKeyStoreEntry() {
private synchronized static SecretKey getOrCreateKeyStoreEntry() {
if (hasKeyStoreEntry()) return getKeyStoreEntry();
else return createKeyStoreEntry();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.dependencies.ManagerScope
import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
import kotlin.coroutines.cancellation.CancellationException

@Singleton
class TokenDataManager @Inject constructor(
private val loginStateRepository: LoginStateRepository,
private val tokenRepository: TokenRepository,
private val tokenRepository: Provider<TokenRepository>,
@param:ManagerScope private val scope: CoroutineScope
) : OnAppStartupComponent {
private val TAG = "TokenDataManager"
Expand Down Expand Up @@ -65,7 +67,7 @@ class TokenDataManager @Inject constructor(
return try {
// Fetch the InfoResponse on an IO dispatcher
val response = withContext(Dispatchers.IO) {
tokenRepository.getInfoResponse()
tokenRepository.get().getInfoResponse()
}
// Ensure the minimum delay to avoid janky UI updates
forceWaitAtLeast500ms(requestStartTimestamp)
Expand All @@ -77,8 +79,10 @@ class TokenDataManager @Inject constructor(
updateLastUpdateTimeMillis()
Log.w(TAG, "Fetched infoResponse: $response")
} catch (e: Exception) {
Log.w(TAG, "InfoResponse error: $e")
_infoResponse.value =InfoResponseState.Failure(e)
if (e is CancellationException) throw e

Log.w(TAG, "InfoResponse error", e)
_infoResponse.value = InfoResponseState.Failure(e)
}
}

Expand Down Expand Up @@ -145,5 +149,4 @@ class TokenDataManager @Inject constructor(
data class Data(val data: InfoResponse) : InfoResponseState()
data class Failure(val exception: Exception) : InfoResponseState()
}

}
Loading