mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-10-25 07:40:36 +00:00
Preserve complete globe map during partial loads
This commit is contained in:
parent
e2989269e0
commit
033e224bc7
@ -38,6 +38,7 @@ import com.bitchat.android.geohash.Geohash
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.ui.globe.GlobeColors
|
||||
import com.bitchat.android.ui.globe.GlobeMapPresentationState
|
||||
import com.bitchat.android.ui.globe.GlobeMapUiState
|
||||
import com.bitchat.android.ui.globe.GlobeState
|
||||
import com.bitchat.android.ui.globe.GlobeTileRequest
|
||||
@ -124,28 +125,29 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
DisposableEffect(mapRepository) {
|
||||
onDispose { mapRepository.close() }
|
||||
}
|
||||
var mapUiState by remember { mutableStateOf(GlobeMapUiState()) }
|
||||
var mapPresentationState by remember {
|
||||
mutableStateOf(GlobeMapPresentationState())
|
||||
}
|
||||
var mapRetryNonce by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(globeState, mapRepository, mapRetryNonce) {
|
||||
if (mapUiState.data.oceanPolygons.isEmpty()) {
|
||||
mapUiState = mapUiState.copy(isLoading = true, hasError = false)
|
||||
if (mapPresentationState.uiState.data.oceanPolygons.isEmpty()) {
|
||||
mapPresentationState = mapPresentationState.startLoading()
|
||||
try {
|
||||
val overview = mapRepository.load(
|
||||
GlobeTileRequest(detailZoom = 0, detailTiles = emptySet())
|
||||
)
|
||||
mapUiState = GlobeMapUiState(
|
||||
data = overview.data,
|
||||
isLoading = false,
|
||||
hasError = false
|
||||
mapPresentationState = mapPresentationState.copy(
|
||||
uiState = GlobeMapUiState(
|
||||
data = overview.data,
|
||||
isLoading = false,
|
||||
hasError = false
|
||||
)
|
||||
)
|
||||
} catch (cancellation: CancellationException) {
|
||||
throw cancellation
|
||||
} catch (_: Exception) {
|
||||
mapUiState = mapUiState.copy(
|
||||
isLoading = false,
|
||||
hasError = true
|
||||
)
|
||||
mapPresentationState = mapPresentationState.showError()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
@ -171,36 +173,30 @@ class GeohashPickerActivity : OrientationAwareActivity() {
|
||||
// in the flow (instead of filtering it) immediately cancels any
|
||||
// obsolete HTTP/decode batch.
|
||||
if (request == null) {
|
||||
mapUiState = mapUiState.copy(isLoading = false)
|
||||
mapPresentationState =
|
||||
mapPresentationState.cancelLoading()
|
||||
return@collectLatest
|
||||
}
|
||||
// Let fast drag/zoom changes settle before starting another batch.
|
||||
delay(MAP_REQUEST_SETTLE_MS)
|
||||
mapUiState = mapUiState.copy(isLoading = true, hasError = false)
|
||||
mapPresentationState = mapPresentationState.startLoading()
|
||||
try {
|
||||
val result = mapRepository.load(request) { partial ->
|
||||
mapUiState = GlobeMapUiState(
|
||||
data = partial.data,
|
||||
isLoading = true,
|
||||
hasError = false
|
||||
)
|
||||
mapPresentationState =
|
||||
mapPresentationState.showPartial(partial)
|
||||
}
|
||||
mapUiState = GlobeMapUiState(
|
||||
data = result.data,
|
||||
isLoading = false,
|
||||
hasError = result.failedTileCount > 0
|
||||
)
|
||||
mapPresentationState =
|
||||
mapPresentationState.showComplete(result)
|
||||
} catch (cancellation: CancellationException) {
|
||||
throw cancellation
|
||||
} catch (_: Exception) {
|
||||
mapUiState = mapUiState.copy(
|
||||
isLoading = false,
|
||||
hasError = true
|
||||
)
|
||||
mapPresentationState = mapPresentationState.showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mapUiState = mapPresentationState.uiState
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val dark = colorScheme.background.luminance() < 0.5f
|
||||
val globeColors = remember(colorScheme, dark) {
|
||||
|
||||
@ -195,3 +195,61 @@ data class GlobeMapUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val hasError: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Keeps progressive tile updates from replacing a complete map with a priority-only
|
||||
* subset. The first request may still render progressively; after one full successful
|
||||
* detail request, cancellation and tile failures retain that complete snapshot.
|
||||
*/
|
||||
internal data class GlobeMapPresentationState(
|
||||
val uiState: GlobeMapUiState = GlobeMapUiState(),
|
||||
val lastCompleteData: GlobeMapData? = null
|
||||
) {
|
||||
fun startLoading(): GlobeMapPresentationState = copy(
|
||||
uiState = uiState.copy(isLoading = true, hasError = false)
|
||||
)
|
||||
|
||||
fun showPartial(result: GlobeMapLoadResult): GlobeMapPresentationState = copy(
|
||||
uiState = GlobeMapUiState(
|
||||
data = lastCompleteData ?: result.data,
|
||||
isLoading = true,
|
||||
hasError = false
|
||||
)
|
||||
)
|
||||
|
||||
fun showComplete(result: GlobeMapLoadResult): GlobeMapPresentationState {
|
||||
val completedWithoutFailures = result.failedTileCount == 0
|
||||
val displayedData = if (completedWithoutFailures) {
|
||||
result.data
|
||||
} else {
|
||||
lastCompleteData ?: result.data
|
||||
}
|
||||
return copy(
|
||||
uiState = GlobeMapUiState(
|
||||
data = displayedData,
|
||||
isLoading = false,
|
||||
hasError = !completedWithoutFailures
|
||||
),
|
||||
lastCompleteData = if (completedWithoutFailures) {
|
||||
result.data
|
||||
} else {
|
||||
lastCompleteData
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelLoading(): GlobeMapPresentationState = copy(
|
||||
uiState = uiState.copy(
|
||||
data = lastCompleteData ?: uiState.data,
|
||||
isLoading = false
|
||||
)
|
||||
)
|
||||
|
||||
fun showError(): GlobeMapPresentationState = copy(
|
||||
uiState = uiState.copy(
|
||||
data = lastCompleteData ?: uiState.data,
|
||||
isLoading = false,
|
||||
hasError = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.bitchat.android.ui.globe
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GlobeMapPresentationStateTest {
|
||||
@Test
|
||||
fun partialUpdateAfterCompleteLoadKeepsCompleteMapThroughCancellation() {
|
||||
val completeData = GlobeMapData(detailZoom = 6)
|
||||
val priorityOnlyData = GlobeMapData(detailZoom = 7)
|
||||
var state = GlobeMapPresentationState().showComplete(
|
||||
result(completeData)
|
||||
)
|
||||
|
||||
state = state.startLoading().showPartial(result(priorityOnlyData))
|
||||
|
||||
assertSame(completeData, state.uiState.data)
|
||||
assertTrue(state.uiState.isLoading)
|
||||
|
||||
state = state.cancelLoading()
|
||||
|
||||
assertSame(completeData, state.uiState.data)
|
||||
assertFalse(state.uiState.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedReplacementAfterCompleteLoadKeepsCompleteMap() {
|
||||
val completeData = GlobeMapData(detailZoom = 6)
|
||||
val incompleteData = GlobeMapData(detailZoom = 7)
|
||||
val state = GlobeMapPresentationState()
|
||||
.showComplete(result(completeData))
|
||||
.showComplete(result(incompleteData, failedTileCount = 1))
|
||||
|
||||
assertSame(completeData, state.uiState.data)
|
||||
assertTrue(state.uiState.hasError)
|
||||
assertSame(completeData, state.lastCompleteData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun firstLoadCanStillDisplayPriorityTilesProgressively() {
|
||||
val priorityOnlyData = GlobeMapData(detailZoom = 4)
|
||||
val state = GlobeMapPresentationState()
|
||||
.showPartial(result(priorityOnlyData))
|
||||
|
||||
assertSame(priorityOnlyData, state.uiState.data)
|
||||
assertTrue(state.uiState.isLoading)
|
||||
}
|
||||
|
||||
private fun result(
|
||||
data: GlobeMapData,
|
||||
failedTileCount: Int = 0
|
||||
): GlobeMapLoadResult = GlobeMapLoadResult(
|
||||
data = data,
|
||||
requestedTileCount = 5,
|
||||
failedTileCount = failedTileCount
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user