Skip to content

Commit 6a30586

Browse files
committed
Merge branch 'dev' into release/1.33.3
2 parents b9aa2d9 + b4b533a commit 6a30586

8 files changed

Lines changed: 191 additions & 52 deletions

File tree

app/src/main/java/org/session/libsession/network/onion/PathManager.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package org.session.libsession.network.onion
22

33
import kotlinx.coroutines.CancellationException
44
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.Deferred
56
import kotlinx.coroutines.FlowPreview
7+
import kotlinx.coroutines.async
68
import kotlinx.coroutines.flow.MutableStateFlow
79
import kotlinx.coroutines.flow.SharingStarted
810
import kotlinx.coroutines.flow.StateFlow
@@ -15,7 +17,7 @@ import kotlinx.coroutines.flow.stateIn
1517
import kotlinx.coroutines.launch
1618
import kotlinx.coroutines.sync.Mutex
1719
import kotlinx.coroutines.sync.withLock
18-
20+
import kotlinx.coroutines.flow.update
1921
import kotlinx.coroutines.withTimeoutOrNull
2022
import org.session.libsession.network.model.Path
2123
import org.session.libsession.network.model.PathStatus
@@ -59,9 +61,7 @@ open class PathManager @Inject constructor(
5961
private val pathSize: Int = 3
6062
private val targetPathCount: Int = 2
6163

62-
private val _paths = MutableStateFlow(
63-
sanitizePaths(storage.getOnionRequestPaths())
64-
)
64+
private val _paths = MutableStateFlow<List<Path>>(emptyList())
6565
val paths: StateFlow<List<Path>> = _paths.asStateFlow()
6666

6767
// Used for synchronization
@@ -91,17 +91,22 @@ open class PathManager @Inject constructor(
9191
if (_paths.value.isEmpty()) PathStatus.ERROR else PathStatus.READY
9292
)
9393

94+
// Warm up from persisted paths without blocking construction.
95+
// Stored as a Deferred so getPath() can await it for deterministic completion.
96+
private val warmUpJob: Deferred<Unit> = scope.async {
97+
val persisted = sanitizePaths(storage.getOnionRequestPaths())
98+
_paths.update { current -> if (current.isEmpty()) persisted else current }
99+
}
100+
94101
init {
95102
// persist to DB whenever paths change
96103
scope.launch {
97104
_paths.drop(1).collectLatest { paths ->
98-
if (paths.isEmpty()) storage.clearOnionRequestPaths()
99-
else {
100-
try {
101-
storage.setOnionRequestPaths(paths)
102-
} catch (e: Exception) {
103-
Log.e("Onion Request", "Failed to persist paths to storage, keeping in-memory only", e)
104-
}
105+
try {
106+
if (paths.isEmpty()) storage.clearOnionRequestPaths()
107+
else storage.setOnionRequestPaths(paths)
108+
} catch (e: Exception) {
109+
Log.e("Onion Request", "Failed to persist paths to storage, keeping in-memory only", e)
105110
}
106111
}
107112
}
@@ -112,6 +117,9 @@ open class PathManager @Inject constructor(
112117
// -----------------------------
113118

114119
suspend fun getPath(exclude: Snode? = null): Path {
120+
// Ensure persisted paths are loaded before checking. No-op after first completion.
121+
warmUpJob.await()
122+
115123
directory.refreshPoolIfStaleAsync()
116124
rotatePathsIfStale()
117125

@@ -374,7 +382,6 @@ open class PathManager @Inject constructor(
374382
}
375383

376384
_paths.value = storage.getOnionRequestPaths()
377-
378385
}
379386
}
380387

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,25 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
13841384
}
13851385
}
13861386
}
1387+
1388+
// bottom search bar
1389+
lifecycleScope.launch {
1390+
repeatOnLifecycle(Lifecycle.State.STARTED) {
1391+
viewModel.searchOpened.collectLatest { isSearchOpen ->
1392+
1393+
if (isSearchOpen) {
1394+
binding.searchBottomBar.visibility = View.VISIBLE
1395+
} else {
1396+
binding.searchBottomBar.visibility = View.GONE
1397+
1398+
adapter.onSearchQueryUpdated(null)
1399+
invalidateOptionsMenu()
1400+
}
1401+
1402+
binding.root.requestApplyInsets()
1403+
}
1404+
}
1405+
}
13871406
}
13881407

13891408
private fun scrollToFirstUnreadMessageOrBottom() {
@@ -3095,36 +3114,44 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
30953114

30963115
// region Search
30973116
private fun setUpSearchResultObserver() {
3098-
searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? ->
3099-
if (result == null) return@Observer
3100-
if (result.getResults().isNotEmpty()) {
3101-
result.getResults()[result.position]?.let {
3102-
if(!gotoMessageById(it.messageId, smoothScroll = false, highlight = true)) {
3103-
searchViewModel.onMissingResult()
3117+
searchViewModel.searchResults.observe(this) { result ->
3118+
if (result == null) return@observe
3119+
3120+
val query = searchViewModel.searchQuery.value
3121+
3122+
try {
3123+
val results = result.getResults()
3124+
val size = results.size
3125+
val position = result.position
3126+
3127+
if (position in 0 until size) {
3128+
results[position]?.let { message ->
3129+
if (!gotoMessageById(message.messageId, smoothScroll = false, highlight = true)) {
3130+
searchViewModel.onMissingResult()
3131+
}
31043132
}
31053133
}
3106-
}
31073134

3108-
binding.searchBottomBar.setData(result.position, result.getResults().size, searchViewModel.searchQuery.value)
3109-
})
3135+
binding.searchBottomBar.setData(position, size, query)
3136+
} catch (e: android.database.StaleDataException) {
3137+
binding.searchBottomBar.setData(0, 0, query)
3138+
}
3139+
}
31103140
}
31113141

31123142
fun onSearchOpened() {
31133143
viewModel.onSearchOpened()
31143144
searchViewModel.onSearchOpened()
3115-
binding.searchBottomBar.visibility = View.VISIBLE
3116-
binding.searchBottomBar.setData(0, 0, searchViewModel.searchQuery.value)
3117-
binding.root.requestApplyInsets()
3118-
3145+
binding.searchBottomBar.setData(
3146+
0,
3147+
0,
3148+
searchViewModel.searchQuery.value
3149+
)
31193150
}
31203151

31213152
fun onSearchClosed() {
31223153
viewModel.onSearchClosed()
31233154
searchViewModel.onSearchClosed()
3124-
binding.searchBottomBar.visibility = View.GONE
3125-
binding.root.requestApplyInsets()
3126-
adapter.onSearchQueryUpdated(null)
3127-
invalidateOptionsMenu()
31283155
}
31293156

31303157
fun onSearchQueryUpdated(query: String) {

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ class ConversationViewModel @AssistedInject constructor(
260260
}
261261

262262
private val _searchOpened = MutableStateFlow(false)
263+
val searchOpened : StateFlow<Boolean> get() = _searchOpened
263264

264265
val appBarData: StateFlow<ConversationAppBarData> = combine(
265266
recipientFlow.repeatedWithEffectiveNotifyTypeChange(),

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ class SearchBottomBar : LinearLayout {
2020

2121
fun initialize() {
2222
binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true)
23+
24+
binding.searchUp.setOnClickListener {
25+
eventListener?.onSearchMoveUpPressed()
26+
}
27+
28+
binding.searchDown.setOnClickListener {
29+
eventListener?.onSearchMoveDownPressed()
30+
}
2331
}
2432

2533
fun setData(position: Int, count: Int, searchQuery: String?) = with(binding) {
2634
binding.loading.visibility = GONE
27-
searchUp.setOnClickListener { v: View? ->
28-
if (eventListener != null) {
29-
eventListener!!.onSearchMoveUpPressed()
30-
}
31-
}
32-
searchDown.setOnClickListener { v: View? ->
33-
if (eventListener != null) {
34-
eventListener!!.onSearchMoveDownPressed()
35-
}
36-
}
3735
if (count > 0) { // we have results
3836
searchPosition.text = resources.getQuantityString(R.plurals.searchMatches, count, position + 1, count)
3937
} else if ( // we have a legitimate query but no results

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class SearchViewModel @Inject constructor(
2727
private val debouncer: Debouncer = Debouncer(200)
2828
private var searchOpen = false
2929
private var activeThreadId: Long = 0
30+
private var currentPosition: Int = 0
3031
val searchResults: LiveData<SearchResult>
3132
get() = result
3233

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

4344
fun onMissingResult() {
4445
if (mutableSearchQuery.value != null) {
45-
updateQuery(mutableSearchQuery.value!!, activeThreadId)
46+
updateQuery(mutableSearchQuery.value!!, activeThreadId, currentPosition)
4647
}
4748
}
4849

4950
fun onMoveUp() {
5051
debouncer.clear()
51-
val messages = result.value!!.getResults() as CursorList<MessageResult?>
52-
val position = Math.min(result.value!!.position + 1, messages.size - 1)
52+
val currentResult = result.value ?: return
53+
val messages = currentResult.getResults() as CursorList<MessageResult?>
54+
val position = minOf(currentResult.position + 1, messages.size - 1)
55+
currentPosition = position
5356
result.setValue(SearchResult(messages, position), false)
5457
}
5558

5659
fun onMoveDown() {
5760
debouncer.clear()
58-
val messages = result.value!!.getResults() as CursorList<MessageResult?>
59-
val position = Math.max(result.value!!.position - 1, 0)
61+
val currentResult = result.value ?: return
62+
val messages = currentResult.getResults() as CursorList<MessageResult?>
63+
val position = maxOf(currentResult.position - 1, 0)
64+
currentPosition = position
6065
result.setValue(SearchResult(messages, position), false)
6166
}
6267

@@ -67,6 +72,7 @@ class SearchViewModel @Inject constructor(
6772
fun onSearchClosed() {
6873
searchOpen = false
6974
mutableSearchQuery.value = null
75+
currentPosition = 0
7076
debouncer.clear()
7177
result.close()
7278
}
@@ -76,11 +82,12 @@ class SearchViewModel @Inject constructor(
7682
result.close()
7783
}
7884

79-
private fun updateQuery(query: String, threadId: Long) {
85+
private fun updateQuery(query: String, threadId: Long, requestedPosition: Int = currentPosition) {
8086
mutableSearchQuery.value = query
8187
activeThreadId = threadId
8288

8389
if(query.length < MIN_QUERY_SIZE) {
90+
currentPosition = 0
8491
result.value = SearchResult(CursorList.emptyList(), 0)
8592
return
8693
}
@@ -89,7 +96,13 @@ class SearchViewModel @Inject constructor(
8996
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
9097
runOnMain {
9198
if (searchOpen && query == mutableSearchQuery.value) {
92-
result.setValue(SearchResult(messages, 0))
99+
val position = if (messages.isEmpty()) {
100+
0
101+
} else {
102+
requestedPosition.coerceIn(0, messages.size - 1)
103+
}
104+
currentPosition = position
105+
result.setValue(SearchResult(messages, position))
93106
} else {
94107
messages.close()
95108
}

app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public static byte[] unseal(@NonNull SealedData sealedData) {
7171
}
7272
}
7373

74-
private static SecretKey getOrCreateKeyStoreEntry() {
74+
private synchronized static SecretKey getOrCreateKeyStoreEntry() {
7575
if (hasKeyStoreEntry()) return getKeyStoreEntry();
7676
else return createKeyStoreEntry();
7777
}

app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenDataManager.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import org.thoughtcrime.securesms.auth.LoginStateRepository
1414
import org.thoughtcrime.securesms.dependencies.ManagerScope
1515
import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent
1616
import javax.inject.Inject
17+
import javax.inject.Provider
1718
import javax.inject.Singleton
19+
import kotlin.coroutines.cancellation.CancellationException
1820

1921
@Singleton
2022
class TokenDataManager @Inject constructor(
2123
private val loginStateRepository: LoginStateRepository,
22-
private val tokenRepository: TokenRepository,
24+
private val tokenRepository: Provider<TokenRepository>,
2325
@param:ManagerScope private val scope: CoroutineScope
2426
) : OnAppStartupComponent {
2527
private val TAG = "TokenDataManager"
@@ -65,7 +67,7 @@ class TokenDataManager @Inject constructor(
6567
return try {
6668
// Fetch the InfoResponse on an IO dispatcher
6769
val response = withContext(Dispatchers.IO) {
68-
tokenRepository.getInfoResponse()
70+
tokenRepository.get().getInfoResponse()
6971
}
7072
// Ensure the minimum delay to avoid janky UI updates
7173
forceWaitAtLeast500ms(requestStartTimestamp)
@@ -77,8 +79,10 @@ class TokenDataManager @Inject constructor(
7779
updateLastUpdateTimeMillis()
7880
Log.w(TAG, "Fetched infoResponse: $response")
7981
} catch (e: Exception) {
80-
Log.w(TAG, "InfoResponse error: $e")
81-
_infoResponse.value =InfoResponseState.Failure(e)
82+
if (e is CancellationException) throw e
83+
84+
Log.w(TAG, "InfoResponse error", e)
85+
_infoResponse.value = InfoResponseState.Failure(e)
8286
}
8387
}
8488

@@ -145,5 +149,4 @@ class TokenDataManager @Inject constructor(
145149
data class Data(val data: InfoResponse) : InfoResponseState()
146150
data class Failure(val exception: Exception) : InfoResponseState()
147151
}
148-
149152
}

0 commit comments

Comments
 (0)