From 1f611f1ec72d6feb7cf6c74df760d59accf0b1cf Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 8 Apr 2026 10:27:39 +1000 Subject: [PATCH 1/7] Helping with early db access --- .../securesms/tokenpage/TokenDataManager.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenDataManager.kt b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenDataManager.kt index b86b5ad3b6..70171b3be6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenDataManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenDataManager.kt @@ -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, @param:ManagerScope private val scope: CoroutineScope ) : OnAppStartupComponent { private val TAG = "TokenDataManager" @@ -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) @@ -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) } } @@ -145,5 +149,4 @@ class TokenDataManager @Inject constructor( data class Data(val data: InfoResponse) : InfoResponseState() data class Failure(val exception: Exception) : InfoResponseState() } - } From 7123d109c96bb1081589b88c4a0e6e786124ab5f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:30:38 +1000 Subject: [PATCH 2/7] Synchronize the keystore creating process (#2114) --- .../java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java index b6a1db9633..26b14731b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java @@ -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(); } From f63dda3a7f9b4afe458344e6112254299b6f0442 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 8 Apr 2026 10:50:59 +1000 Subject: [PATCH 3/7] Missing file --- .../libsession/network/onion/PathManager.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/session/libsession/network/onion/PathManager.kt b/app/src/main/java/org/session/libsession/network/onion/PathManager.kt index b0a9a3bf8b..3970431217 100644 --- a/app/src/main/java/org/session/libsession/network/onion/PathManager.kt +++ b/app/src/main/java/org/session/libsession/network/onion/PathManager.kt @@ -15,6 +15,9 @@ 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.withContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withTimeoutOrNull import org.session.libsession.network.model.Path @@ -59,9 +62,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>(emptyList()) val paths: StateFlow> = _paths.asStateFlow() // Used for synchronization @@ -92,16 +93,22 @@ open class PathManager @Inject constructor( ) init { + // Warm up from persisted paths without blocking construction + scope.launch { + val persisted = withContext(Dispatchers.IO) { + sanitizePaths(storage.getOnionRequestPaths()) + } + _paths.update { current -> if (current.isEmpty()) persisted else current } + } + // 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) } } } @@ -374,7 +381,6 @@ open class PathManager @Inject constructor( } _paths.value = storage.getOnionRequestPaths() - } } From ff44a46182d698be70564e84958a912515e67e00 Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 8 Apr 2026 09:57:19 +0800 Subject: [PATCH 4/7] Added landscape for search bottom bar, preserve visibility accross rotation --- .../conversation/v2/ConversationActivityV2.kt | 61 +++++++++---- .../conversation/v2/ConversationViewModel.kt | 1 + .../conversation/v2/search/SearchBottomBar.kt | 18 ++-- .../layout-land/view_search_bottom_bar.xml | 90 +++++++++++++++++++ 4 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/layout-land/view_search_bottom_bar.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 78e1f81051..393b23457f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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() { @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index e2837ca3eb..cabf3915f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -260,6 +260,7 @@ class ConversationViewModel @AssistedInject constructor( } private val _searchOpened = MutableStateFlow(false) + val searchOpened : StateFlow get() = _searchOpened val appBarData: StateFlow = combine( recipientFlow.repeatedWithEffectiveNotifyTypeChange(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt index 53642aca3d..536fcfa5ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt @@ -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 diff --git a/app/src/main/res/layout-land/view_search_bottom_bar.xml b/app/src/main/res/layout-land/view_search_bottom_bar.xml new file mode 100644 index 0000000000..1cbb5e4fcb --- /dev/null +++ b/app/src/main/res/layout-land/view_search_bottom_bar.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 5cdb75491c234019bd77f5d2d6df225d10b12d9c Mon Sep 17 00:00:00 2001 From: jbsession Date: Wed, 8 Apr 2026 10:03:46 +0800 Subject: [PATCH 5/7] Preserve position --- .../conversation/v2/search/SearchViewModel.kt | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt index 6b315ceeb6..1786f0fe92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt @@ -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 get() = result @@ -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 - val position = Math.min(result.value!!.position + 1, messages.size - 1) + val currentResult = result.value ?: return + val messages = currentResult.getResults() as CursorList + 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 - val position = Math.max(result.value!!.position - 1, 0) + val currentResult = result.value ?: return + val messages = currentResult.getResults() as CursorList + val position = maxOf(currentResult.position - 1, 0) + currentPosition = position result.setValue(SearchResult(messages, position), false) } @@ -67,6 +72,7 @@ class SearchViewModel @Inject constructor( fun onSearchClosed() { searchOpen = false mutableSearchQuery.value = null + currentPosition = 0 debouncer.clear() result.close() } @@ -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 } @@ -89,7 +96,13 @@ class SearchViewModel @Inject constructor( searchRepository.query(query, threadId) { messages: CursorList -> 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() } From a7d219808bc7df1510cabf3a35340842f332ed70 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 8 Apr 2026 12:06:15 +1000 Subject: [PATCH 6/7] Fix test --- .../libsession/network/onion/PathManager.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/session/libsession/network/onion/PathManager.kt b/app/src/main/java/org/session/libsession/network/onion/PathManager.kt index 3970431217..fb4e5679b4 100644 --- a/app/src/main/java/org/session/libsession/network/onion/PathManager.kt +++ b/app/src/main/java/org/session/libsession/network/onion/PathManager.kt @@ -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 @@ -16,9 +18,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext -import kotlinx.coroutines.Dispatchers - import kotlinx.coroutines.withTimeoutOrNull import org.session.libsession.network.model.Path import org.session.libsession.network.model.PathStatus @@ -92,15 +91,14 @@ open class PathManager @Inject constructor( if (_paths.value.isEmpty()) PathStatus.ERROR else PathStatus.READY ) - init { - // Warm up from persisted paths without blocking construction - scope.launch { - val persisted = withContext(Dispatchers.IO) { - sanitizePaths(storage.getOnionRequestPaths()) - } - _paths.update { current -> if (current.isEmpty()) persisted else current } - } + // Warm up from persisted paths without blocking construction. + // Stored as a Deferred so getPath() can await it for deterministic completion. + private val warmUpJob: Deferred = 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 -> @@ -119,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() From b9aa2d9d3b5ed591e8a5014360fc8ac00cb30cdb Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Wed, 8 Apr 2026 12:52:40 +1000 Subject: [PATCH 7/7] Version bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2121f66b5c..0c5f45a3fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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(