mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Gate nearby notes behind tap-to-reveal consent (#771)
* Add nearby notes tap-to-reveal consent * Stop nearby notes while app is backgrounded
This commit is contained in:
parent
92d07b22fa
commit
d615fc9cc0
@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
*/
|
||||
@MainThread
|
||||
class LocationNotesManager private constructor() {
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocationNotesManager"
|
||||
private const val MAX_NOTES_IN_MEMORY = 500
|
||||
@ -27,7 +27,7 @@ class LocationNotesManager private constructor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Note data class matching iOS implementation
|
||||
*/
|
||||
@ -94,6 +94,8 @@ class LocationNotesManager private constructor() {
|
||||
|
||||
// Coroutine scope for background operations
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var subscribeRetryJob: Job? = null
|
||||
private var initialLoadJob: Job? = null
|
||||
|
||||
/**
|
||||
* Initialize dependencies
|
||||
@ -289,6 +291,11 @@ class LocationNotesManager private constructor() {
|
||||
* Subscribe to location notes for current geohash
|
||||
*/
|
||||
private fun subscribeAll() {
|
||||
subscribeRetryJob?.cancel()
|
||||
subscribeRetryJob = null
|
||||
initialLoadJob?.cancel()
|
||||
initialLoadJob = null
|
||||
|
||||
val currentGeohash = _geohash.value
|
||||
if (currentGeohash == null) {
|
||||
Log.w(TAG, "Cannot subscribe - no geohash set")
|
||||
@ -301,7 +308,7 @@ class LocationNotesManager private constructor() {
|
||||
Log.e(TAG, "Cannot subscribe - subscribe function not initialized; will retry shortly")
|
||||
_state.value = State.LOADING
|
||||
// Retry a few times in case initialization is racing the sheet open
|
||||
scope.launch {
|
||||
subscribeRetryJob = scope.launch {
|
||||
var attempts = 0
|
||||
while (attempts < 10 && subscribeFunc == null) {
|
||||
delay(300)
|
||||
@ -342,9 +349,9 @@ class LocationNotesManager private constructor() {
|
||||
}
|
||||
|
||||
// Mark initial load complete after brief delay to allow relay responses
|
||||
scope.launch {
|
||||
initialLoadJob = scope.launch {
|
||||
delay(2000) // Wait 2 seconds for initial batch
|
||||
if (!_initialLoadComplete.value!!) {
|
||||
if (_geohash.value == currentGeohash && !_initialLoadComplete.value) {
|
||||
_initialLoadComplete.value = true
|
||||
_state.value = State.READY
|
||||
Log.d(TAG, "Initial load complete for geohash: $currentGeohash (${noteIDs.size} notes)")
|
||||
@ -441,6 +448,11 @@ class LocationNotesManager private constructor() {
|
||||
* Cancel subscription and clear state
|
||||
*/
|
||||
fun cancel() {
|
||||
subscribeRetryJob?.cancel()
|
||||
subscribeRetryJob = null
|
||||
initialLoadJob?.cancel()
|
||||
initialLoadJob = null
|
||||
|
||||
if (subscriptionIDs.isNotEmpty()) {
|
||||
subscriptionIDs.values.forEach { subId ->
|
||||
try {
|
||||
@ -453,17 +465,26 @@ class LocationNotesManager private constructor() {
|
||||
subscribedGeohashes = emptySet()
|
||||
_state.value = State.IDLE
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
* End the nearby-notes session and discard location-correlated UI state.
|
||||
* Unlike [cancel], this also clears the target so a later activation can
|
||||
* safely subscribe to the same building geohash again.
|
||||
*/
|
||||
fun cleanup() {
|
||||
fun stop() {
|
||||
cancel()
|
||||
scope.cancel()
|
||||
_notes.value = emptyList()
|
||||
noteIDs.clear()
|
||||
_geohash.value = null
|
||||
_initialLoadComplete.value = false
|
||||
_errorMessage.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
fun cleanup() {
|
||||
stop()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import com.bitchat.android.geohash.GeohashChannel
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Session-scoped consent gate for nearby location notes.
|
||||
*
|
||||
* Merely rendering the mesh timeline must not open a building-precision Nostr
|
||||
* subscription. A subscription is eligible only after an explicit reveal and
|
||||
* while the app is foregrounded and at least one nearby-notes surface is active.
|
||||
*/
|
||||
@MainThread
|
||||
class NearbyNotesController internal constructor(
|
||||
private val subscribe: (String) -> Unit,
|
||||
private val unsubscribe: () -> Unit,
|
||||
) {
|
||||
private val _revealed = MutableStateFlow(false)
|
||||
val revealed: StateFlow<Boolean> = _revealed.asStateFlow()
|
||||
|
||||
private var activeHolders = 0
|
||||
private var locationEnabled = false
|
||||
private var locationAuthorized = false
|
||||
private var appForeground = false
|
||||
private var buildingGeohash: String? = null
|
||||
private var subscribedGeohash: String? = null
|
||||
|
||||
/**
|
||||
* Unlocks nearby notes for this process session. Deactivation deliberately
|
||||
* does not reset consent, matching the iOS privacy model.
|
||||
*/
|
||||
fun reveal() {
|
||||
if (_revealed.value) return
|
||||
_revealed.value = true
|
||||
reconcileSubscription()
|
||||
}
|
||||
|
||||
/** Holds the subscription while a nearby-notes surface is visible. */
|
||||
fun activate() {
|
||||
activeHolders += 1
|
||||
reconcileSubscription()
|
||||
}
|
||||
|
||||
/** Releases a matching [activate] hold and unsubscribes after the last one. */
|
||||
fun deactivate() {
|
||||
activeHolders = (activeHolders - 1).coerceAtLeast(0)
|
||||
reconcileSubscription()
|
||||
}
|
||||
|
||||
/** Closes the live subscription whenever the process leaves the foreground. */
|
||||
fun updateAppForeground(isForeground: Boolean) {
|
||||
appForeground = isForeground
|
||||
reconcileSubscription()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the privacy-sensitive inputs independently of view activation.
|
||||
* Permission revocation, location disable, or loss of the building cell
|
||||
* immediately closes any live subscription.
|
||||
*/
|
||||
fun updateAvailability(
|
||||
locationEnabled: Boolean,
|
||||
locationAuthorized: Boolean,
|
||||
buildingGeohash: String?,
|
||||
) {
|
||||
this.locationEnabled = locationEnabled
|
||||
this.locationAuthorized = locationAuthorized
|
||||
this.buildingGeohash = buildingGeohash
|
||||
?.trim()
|
||||
?.lowercase()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
reconcileSubscription()
|
||||
}
|
||||
|
||||
fun offersRevealHint(): Boolean =
|
||||
!_revealed.value &&
|
||||
locationEnabled &&
|
||||
locationAuthorized &&
|
||||
buildingGeohash != null
|
||||
|
||||
private fun reconcileSubscription() {
|
||||
val target = buildingGeohash.takeIf {
|
||||
activeHolders > 0 &&
|
||||
appForeground &&
|
||||
_revealed.value &&
|
||||
locationEnabled &&
|
||||
locationAuthorized
|
||||
}
|
||||
|
||||
if (subscribedGeohash != null && subscribedGeohash != target) {
|
||||
unsubscribe()
|
||||
subscribedGeohash = null
|
||||
}
|
||||
|
||||
if (target != null && subscribedGeohash == null) {
|
||||
subscribe(target)
|
||||
subscribedGeohash = target
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val shared: NearbyNotesController by lazy {
|
||||
val manager = LocationNotesManager.getInstance()
|
||||
NearbyNotesController(
|
||||
subscribe = manager::setGeohash,
|
||||
unsubscribe = manager::stop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Building precision is location-notes precision and remains private before a
|
||||
* reveal. Explicit bookmarks remain eligible because saving one is itself an
|
||||
* intentional location act.
|
||||
*/
|
||||
internal fun geohashesForSampling(
|
||||
availableChannels: List<GeohashChannel>,
|
||||
bookmarks: Collection<String>,
|
||||
notesRevealed: Boolean,
|
||||
): List<String> = buildSet {
|
||||
availableChannels
|
||||
.filter { notesRevealed || it.level != GeohashChannelLevel.BUILDING }
|
||||
.mapTo(this) { it.geohash }
|
||||
addAll(bookmarks)
|
||||
}.toList()
|
||||
@ -12,19 +12,35 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.geohash.ChannelID
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
import com.bitchat.android.nostr.NearbyNotesController
|
||||
import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||
|
||||
/**
|
||||
@ -86,6 +102,67 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
|
||||
// Get location channel info for timeline switching
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val nearbyNotesController = remember { NearbyNotesController.shared }
|
||||
val nearbyNotesRevealed by nearbyNotesController.revealed.collectAsStateWithLifecycle()
|
||||
val locationPermissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val locationEnabled by locationManager.effectiveLocationEnabled.collectAsStateWithLifecycle(false)
|
||||
val availableLocationChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val nearbyNotes by remember { LocationNotesManager.getInstance() }
|
||||
.notes
|
||||
.collectAsStateWithLifecycle()
|
||||
val buildingGeohash = availableLocationChannels
|
||||
.firstOrNull { it.level == GeohashChannelLevel.BUILDING }
|
||||
?.geohash
|
||||
val isMeshTimeline =
|
||||
currentChannel == null &&
|
||||
selectedLocationChannel is ChannelID.Mesh &&
|
||||
selectedPrivatePeer == null &&
|
||||
privateChatSheetPeer == null
|
||||
|
||||
val processLifecycleOwner = remember { ProcessLifecycleOwner.get() }
|
||||
DisposableEffect(processLifecycleOwner, nearbyNotesController) {
|
||||
val lifecycle = processLifecycleOwner.lifecycle
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
nearbyNotesController.updateAppForeground(true)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
nearbyNotesController.updateAppForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle.addObserver(observer)
|
||||
nearbyNotesController.updateAppForeground(
|
||||
lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED),
|
||||
)
|
||||
|
||||
onDispose {
|
||||
lifecycle.removeObserver(observer)
|
||||
nearbyNotesController.updateAppForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(
|
||||
isMeshTimeline,
|
||||
locationEnabled,
|
||||
locationPermissionState,
|
||||
buildingGeohash,
|
||||
nearbyNotesController,
|
||||
) {
|
||||
nearbyNotesController.updateAvailability(
|
||||
locationEnabled = locationEnabled,
|
||||
locationAuthorized =
|
||||
locationPermissionState == LocationChannelManager.PermissionState.AUTHORIZED,
|
||||
buildingGeohash = buildingGeohash,
|
||||
)
|
||||
if (isMeshTimeline) nearbyNotesController.activate()
|
||||
onDispose {
|
||||
if (isMeshTimeline) nearbyNotesController.deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what messages to show based on current context (unified timelines)
|
||||
// Legacy private chat timeline removed - private chats now exclusively use PrivateChatSheet
|
||||
@ -131,58 +208,91 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
)
|
||||
|
||||
// Messages area - takes up available space, will compress when keyboard appears
|
||||
MessagesList(
|
||||
messages = displayMessages,
|
||||
currentUserNickname = nickname,
|
||||
meshService = viewModel.meshServiceFacade,
|
||||
modifier = Modifier.weight(1f),
|
||||
forceScrollToBottom = forceScrollToBottom,
|
||||
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||
onNicknameClick = { fullSenderName ->
|
||||
// Single click - mention user in text input
|
||||
val currentText = messageText.text
|
||||
|
||||
// Extract base nickname and hash suffix from full sender name
|
||||
val (baseName, hashSuffix) = splitSuffix(fullSenderName)
|
||||
|
||||
// Check if we're in a geohash channel to include hash suffix
|
||||
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
||||
val mentionText = if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location && hashSuffix.isNotEmpty()) {
|
||||
// In geohash chat - include the hash suffix from the full display name
|
||||
"@$baseName$hashSuffix"
|
||||
} else {
|
||||
// Regular chat - just the base nickname
|
||||
"@$baseName"
|
||||
}
|
||||
|
||||
val newText = when {
|
||||
currentText.isEmpty() -> "$mentionText "
|
||||
currentText.endsWith(" ") -> "$currentText$mentionText "
|
||||
else -> "$currentText $mentionText "
|
||||
}
|
||||
|
||||
messageText = TextFieldValue(
|
||||
text = newText,
|
||||
selection = TextRange(newText.length)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (isMeshTimeline && nearbyNotesRevealed && nearbyNotes.isNotEmpty()) {
|
||||
NearbyNotesStrip(
|
||||
noteCount = nearbyNotes.size,
|
||||
onClick = { showLocationNotesSheet = true },
|
||||
)
|
||||
},
|
||||
onMessageLongPress = { message ->
|
||||
// Message long press - open user action sheet with message context
|
||||
// Extract base nickname from message sender (contains all necessary info)
|
||||
val (baseName, _) = splitSuffix(message.sender)
|
||||
selectedUserForSheet = baseName
|
||||
selectedMessageForSheet = message
|
||||
showUserSheet = true
|
||||
},
|
||||
onCancelTransfer = { msg ->
|
||||
viewModel.cancelMediaSend(msg.id)
|
||||
},
|
||||
onImageClick = { currentPath, allImagePaths, initialIndex ->
|
||||
viewerImagePaths = allImagePaths
|
||||
initialViewerIndex = initialIndex
|
||||
showFullScreenImageViewer = true
|
||||
}
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
MessagesList(
|
||||
messages = displayMessages,
|
||||
currentUserNickname = nickname,
|
||||
meshService = viewModel.meshServiceFacade,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
forceScrollToBottom = forceScrollToBottom,
|
||||
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||
onNicknameClick = { fullSenderName ->
|
||||
// Single click - mention user in text input
|
||||
val currentText = messageText.text
|
||||
|
||||
// Extract base nickname and hash suffix from full sender name
|
||||
val (baseName, hashSuffix) = splitSuffix(fullSenderName)
|
||||
|
||||
// Check if we're in a geohash channel to include hash suffix
|
||||
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
||||
val mentionText = if (
|
||||
selectedLocationChannel is ChannelID.Location &&
|
||||
hashSuffix.isNotEmpty()
|
||||
) {
|
||||
// In geohash chat - include the hash suffix from the full display name
|
||||
"@$baseName$hashSuffix"
|
||||
} else {
|
||||
// Regular chat - just the base nickname
|
||||
"@$baseName"
|
||||
}
|
||||
|
||||
val newText = when {
|
||||
currentText.isEmpty() -> "$mentionText "
|
||||
currentText.endsWith(" ") -> "$currentText$mentionText "
|
||||
else -> "$currentText $mentionText "
|
||||
}
|
||||
|
||||
messageText = TextFieldValue(
|
||||
text = newText,
|
||||
selection = TextRange(newText.length),
|
||||
)
|
||||
},
|
||||
onMessageLongPress = { message ->
|
||||
// Message long press - open user action sheet with message context
|
||||
// Extract base nickname from message sender (contains all necessary info)
|
||||
val (baseName, _) = splitSuffix(message.sender)
|
||||
selectedUserForSheet = baseName
|
||||
selectedMessageForSheet = message
|
||||
showUserSheet = true
|
||||
},
|
||||
onCancelTransfer = { msg ->
|
||||
viewModel.cancelMediaSend(msg.id)
|
||||
},
|
||||
onImageClick = { currentPath, allImagePaths, initialIndex ->
|
||||
viewerImagePaths = allImagePaths
|
||||
initialViewerIndex = initialIndex
|
||||
showFullScreenImageViewer = true
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
displayMessages.isEmpty() &&
|
||||
isMeshTimeline &&
|
||||
!nearbyNotesRevealed &&
|
||||
locationEnabled &&
|
||||
locationPermissionState ==
|
||||
LocationChannelManager.PermissionState.AUTHORIZED &&
|
||||
buildingGeohash != null
|
||||
) {
|
||||
NearbyNotesRevealHint(
|
||||
onClick = nearbyNotesController::reveal,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Input area - stays at bottom
|
||||
// Bridge file share from lower-level input to ViewModel
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||
@ -253,7 +363,10 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
onShowAppInfo = { viewModel.showAppInfo() },
|
||||
onPanicClear = { viewModel.panicClearAllData() },
|
||||
onLocationChannelsClick = { showLocationChannelsSheet = true },
|
||||
onLocationNotesClick = { showLocationNotesSheet = true }
|
||||
onLocationNotesClick = {
|
||||
nearbyNotesController.reveal()
|
||||
showLocationNotesSheet = true
|
||||
}
|
||||
)
|
||||
|
||||
// Divider under header - positioned after status bar + header height
|
||||
@ -374,6 +487,68 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NearbyNotesRevealHint(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val actionLabel = stringResource(R.string.nearby_notes_reveal)
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.padding(horizontal = 24.dp)
|
||||
.semantics { contentDescription = actionLabel },
|
||||
) {
|
||||
Text(
|
||||
text = "📍 $actionLabel",
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NearbyNotesStrip(
|
||||
noteCount: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.padding(horizontal = 12.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "📍 " + if (noteCount == 1) {
|
||||
stringResource(R.string.nearby_notes_one)
|
||||
} else {
|
||||
stringResource(R.string.nearby_notes_many, noteCount)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Text(
|
||||
text = "›",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInputSection(
|
||||
messageText: TextFieldValue,
|
||||
|
||||
@ -34,6 +34,8 @@ import com.bitchat.android.geohash.GeohashChannel
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.geohash.GeohashBookmarksStore
|
||||
import com.bitchat.android.nostr.NearbyNotesController
|
||||
import com.bitchat.android.nostr.geohashesForSampling
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -61,6 +63,7 @@ fun LocationChannelsSheet(
|
||||
// Observe location manager state
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val notesRevealed by NearbyNotesController.shared.revealed.collectAsStateWithLifecycle()
|
||||
val selectedChannel by locationManager.selectedChannel.collectAsStateWithLifecycle()
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val appLocationEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle()
|
||||
@ -534,9 +537,13 @@ fun LocationChannelsSheet(
|
||||
}
|
||||
|
||||
// Sampling management: update sampling when channels/bookmarks change
|
||||
LaunchedEffect(isPresented, availableChannels, bookmarks) {
|
||||
LaunchedEffect(isPresented, availableChannels, bookmarks, notesRevealed) {
|
||||
if (isPresented) {
|
||||
val geohashes = (availableChannels.map { it.geohash } + bookmarks).toSet().toList()
|
||||
val geohashes = geohashesForSampling(
|
||||
availableChannels = availableChannels,
|
||||
bookmarks = bookmarks,
|
||||
notesRevealed = notesRevealed,
|
||||
)
|
||||
viewModel.beginGeohashSampling(geohashes)
|
||||
} else {
|
||||
viewModel.endGeohashSampling()
|
||||
|
||||
@ -32,6 +32,7 @@ import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
import com.bitchat.android.nostr.NearbyNotesController
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
@ -58,12 +59,15 @@ fun LocationNotesSheet(
|
||||
// Managers
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val nearbyNotesController = remember { NearbyNotesController.shared }
|
||||
|
||||
// State
|
||||
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||
val state by notesManager.state.collectAsStateWithLifecycle(LocationNotesManager.State.IDLE)
|
||||
val errorMessage by notesManager.errorMessage.collectAsStateWithLifecycle()
|
||||
val initialLoadComplete by notesManager.initialLoadComplete.collectAsStateWithLifecycle(false)
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val locationEnabled by locationManager.effectiveLocationEnabled.collectAsStateWithLifecycle(false)
|
||||
|
||||
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
|
||||
val count = notes.size
|
||||
@ -94,15 +98,24 @@ fun LocationNotesSheet(
|
||||
locationManager.refreshChannels()
|
||||
}
|
||||
|
||||
// Effect to set geohash when sheet opens
|
||||
LaunchedEffect(geohash) {
|
||||
notesManager.setGeohash(geohash)
|
||||
}
|
||||
|
||||
// Cleanup when sheet closes
|
||||
DisposableEffect(Unit) {
|
||||
// Opening the notes sheet is an explicit reveal. The balanced hold lets
|
||||
// the mesh timeline keep the shared subscription alive after dismissal.
|
||||
DisposableEffect(
|
||||
geohash,
|
||||
locationEnabled,
|
||||
permissionState,
|
||||
nearbyNotesController,
|
||||
) {
|
||||
nearbyNotesController.updateAvailability(
|
||||
locationEnabled = locationEnabled,
|
||||
locationAuthorized =
|
||||
permissionState == LocationChannelManager.PermissionState.AUTHORIZED,
|
||||
buildingGeohash = geohash,
|
||||
)
|
||||
nearbyNotesController.activate()
|
||||
nearbyNotesController.reveal()
|
||||
onDispose {
|
||||
notesManager.cancel()
|
||||
nearbyNotesController.deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">فتح قسم حول</string>
|
||||
<string name="nearby_notes_reveal">تحقّق من الملاحظات المتروكة هنا</string>
|
||||
<string name="nearby_notes_one">تُركت ملاحظة واحدة هنا — انقر للقراءة</string>
|
||||
<string name="nearby_notes_many">تُركت %d ملاحظات هنا — انقر للقراءة</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">পরিচিতি খুলুন</string>
|
||||
<string name="nearby_notes_reveal">এখানে রাখা নোট আছে কি না দেখুন</string>
|
||||
<string name="nearby_notes_one">এখানে 1টি নোট রাখা আছে — পড়তে ট্যাপ করুন</string>
|
||||
<string name="nearby_notes_many">এখানে %dটি নোট রাখা আছে — পড়তে ট্যাপ করুন</string>
|
||||
</resources>
|
||||
|
||||
@ -402,4 +402,7 @@
|
||||
<string name="verify_success_body">Du hast %1$s verifiziert</string>
|
||||
<string name="verify_success_system_message">verifiziert %1$s</string>
|
||||
<string name="cd_open_about">Info öffnen</string>
|
||||
<string name="nearby_notes_reveal">nachsehen, ob hier notizen hinterlassen wurden</string>
|
||||
<string name="nearby_notes_one">1 notiz hier hinterlassen — tippen zum lesen</string>
|
||||
<string name="nearby_notes_many">%d notizen hier hinterlassen — tippen zum lesen</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">Verificaste a %1$s</string>
|
||||
<string name="verify_success_system_message">verificado %1$s</string>
|
||||
<string name="cd_open_about">Abrir Acerca de</string>
|
||||
<string name="nearby_notes_reveal">buscar notas dejadas aquí</string>
|
||||
<string name="nearby_notes_one">1 nota dejada aquí — toca para leer</string>
|
||||
<string name="nearby_notes_many">%d notas dejadas aquí — toca para leer</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">باز کردن درباره</string>
|
||||
<string name="nearby_notes_reveal">یادداشتهای باقیمانده در اینجا را بررسی کنید</string>
|
||||
<string name="nearby_notes_one">۱ یادداشت اینجا باقی مانده — برای خواندن ضربه بزنید</string>
|
||||
<string name="nearby_notes_many">%d یادداشت اینجا باقی مانده — برای خواندن ضربه بزنید</string>
|
||||
</resources>
|
||||
|
||||
@ -400,4 +400,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Buksan ang Tungkol</string>
|
||||
<string name="nearby_notes_reveal">tingnan kung may mga note na naiwan dito</string>
|
||||
<string name="nearby_notes_one">1 note ang naiwan dito — i-tap para basahin</string>
|
||||
<string name="nearby_notes_many">%d note ang naiwan dito — i-tap para basahin</string>
|
||||
</resources>
|
||||
|
||||
@ -414,4 +414,7 @@
|
||||
<string name="verify_success_body">Vous avez vérifié %1$s</string>
|
||||
<string name="verify_success_system_message">vérifié %1$s</string>
|
||||
<string name="cd_open_about">Ouvrir À propos</string>
|
||||
<string name="nearby_notes_reveal">vérifier s\'il y a des notes laissées ici</string>
|
||||
<string name="nearby_notes_one">1 note laissée ici — appuyez pour lire</string>
|
||||
<string name="nearby_notes_many">%d notes laissées ici — appuyez pour lire</string>
|
||||
</resources>
|
||||
|
||||
@ -54,4 +54,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">פתיחת אודות</string>
|
||||
<string name="nearby_notes_reveal">בדיקה אם הושארו כאן פתקים</string>
|
||||
<string name="nearby_notes_one">פתק אחד הושאר כאן — הקש לקריאה</string>
|
||||
<string name="nearby_notes_many">%d פתקים הושארו כאן — הקש לקריאה</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">परिचय खोलें</string>
|
||||
<string name="nearby_notes_reveal">देखें कि यहाँ नोट छोड़े गए हैं या नहीं</string>
|
||||
<string name="nearby_notes_one">यहाँ 1 नोट छोड़ा गया है — पढ़ने के लिए टैप करें</string>
|
||||
<string name="nearby_notes_many">यहाँ %d नोट छोड़े गए हैं — पढ़ने के लिए टैप करें</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Buka Tentang</string>
|
||||
<string name="nearby_notes_reveal">periksa catatan yang ditinggalkan di sini</string>
|
||||
<string name="nearby_notes_one">1 catatan ditinggalkan di sini — ketuk untuk membaca</string>
|
||||
<string name="nearby_notes_many">%d catatan ditinggalkan di sini — ketuk untuk membaca</string>
|
||||
</resources>
|
||||
|
||||
@ -434,4 +434,7 @@
|
||||
<string name="verify_success_body">Hai verificato %1$s</string>
|
||||
<string name="verify_success_system_message">verificato %1$s</string>
|
||||
<string name="cd_open_about">Apri Informazioni</string>
|
||||
<string name="nearby_notes_reveal">controlla se ci sono note lasciate qui</string>
|
||||
<string name="nearby_notes_one">1 nota lasciata qui — tocca per leggere</string>
|
||||
<string name="nearby_notes_many">%d note lasciate qui — tocca per leggere</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">%1$s を検証しました</string>
|
||||
<string name="verify_success_system_message">%1$s を検証しました</string>
|
||||
<string name="cd_open_about">このアプリについてを開く</string>
|
||||
<string name="nearby_notes_reveal">ここに残されたメモを確認</string>
|
||||
<string name="nearby_notes_one">ここに1件のメモがあります — タップして読む</string>
|
||||
<string name="nearby_notes_many">ここに%d件のメモがあります — タップして読む</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">აპის შესახებ გახსნა</string>
|
||||
<string name="nearby_notes_reveal">აქ დატოვებული ჩანაწერების შემოწმება</string>
|
||||
<string name="nearby_notes_one">აქ 1 ჩანაწერია დატოვებული — წასაკითხად შეეხეთ</string>
|
||||
<string name="nearby_notes_many">აქ %d ჩანაწერია დატოვებული — წასაკითხად შეეხეთ</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">정보 열기</string>
|
||||
<string name="nearby_notes_reveal">여기 남겨진 쪽지 확인</string>
|
||||
<string name="nearby_notes_one">여기 남겨진 쪽지 1개 — 탭하여 읽기</string>
|
||||
<string name="nearby_notes_many">여기 남겨진 쪽지 %d개 — 탭하여 읽기</string>
|
||||
</resources>
|
||||
|
||||
@ -414,4 +414,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Sokafy ny momba</string>
|
||||
<string name="nearby_notes_reveal">hizaha raha misy naoty navela teto</string>
|
||||
<string name="nearby_notes_one">naoty 1 no navela teto — tsindrio raha hamaky</string>
|
||||
<string name="nearby_notes_many">naoty %d no navela teto — tsindrio raha hamaky</string>
|
||||
</resources>
|
||||
|
||||
@ -41,4 +41,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Buka Perihal</string>
|
||||
<string name="nearby_notes_reveal">semak nota yang ditinggalkan di sini</string>
|
||||
<string name="nearby_notes_one">1 nota ditinggalkan di sini — ketik untuk baca</string>
|
||||
<string name="nearby_notes_many">%d nota ditinggalkan di sini — ketik untuk baca</string>
|
||||
</resources>
|
||||
|
||||
@ -400,4 +400,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">परिचय खोल्नुहोस्</string>
|
||||
<string name="nearby_notes_reveal">यहाँ छोडिएका नोटहरू छन् कि हेर्नुहोस्</string>
|
||||
<string name="nearby_notes_one">यहाँ 1 नोट छोडिएको छ — पढ्न ट्याप गर्नुहोस्</string>
|
||||
<string name="nearby_notes_many">यहाँ %d नोटहरू छोडिएका छन् — पढ्न ट्याप गर्नुहोस्</string>
|
||||
</resources>
|
||||
|
||||
@ -432,4 +432,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Info openen</string>
|
||||
<string name="nearby_notes_reveal">kijk of hier notities zijn achtergelaten</string>
|
||||
<string name="nearby_notes_one">1 notitie hier achtergelaten — tik om te lezen</string>
|
||||
<string name="nearby_notes_many">%d notities hier achtergelaten — tik om te lezen</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">ایپ بارے کھولو</string>
|
||||
<string name="nearby_notes_reveal">ایتھے چھڈے نوٹس ویکھو</string>
|
||||
<string name="nearby_notes_one">ایتھے 1 نوٹ چھڈیا گیا — پڑھݨ لئی ٹیپ کرو</string>
|
||||
<string name="nearby_notes_many">ایتھے %d نوٹس چھڈے گئے — پڑھݨ لئی ٹیپ کرو</string>
|
||||
</resources>
|
||||
|
||||
@ -54,4 +54,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Otwórz informacje</string>
|
||||
<string name="nearby_notes_reveal">sprawdź, czy zostawiono tutaj notatki</string>
|
||||
<string name="nearby_notes_one">1 notatka zostawiona tutaj — stuknij, aby przeczytać</string>
|
||||
<string name="nearby_notes_many">%d notatek zostawionych tutaj — stuknij, aby przeczytać</string>
|
||||
</resources>
|
||||
|
||||
@ -400,4 +400,7 @@
|
||||
<string name="verify_success_title">Verificado</string>
|
||||
<string name="verify_success_body">Você verificou %1$s</string>
|
||||
<string name="verify_success_system_message">verificou %1$s</string>
|
||||
<string name="nearby_notes_reveal">ver se há notas deixadas aqui</string>
|
||||
<string name="nearby_notes_one">1 nota deixada aqui — toque para ler</string>
|
||||
<string name="nearby_notes_many">%d notas deixadas aqui — toque para ler</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">Você verificou %1$s</string>
|
||||
<string name="verify_success_system_message">verificou %1$s</string>
|
||||
<string name="cd_open_about">Abrir Sobre</string>
|
||||
<string name="nearby_notes_reveal">ver se há notas deixadas aqui</string>
|
||||
<string name="nearby_notes_one">1 nota deixada aqui — toque para ler</string>
|
||||
<string name="nearby_notes_many">%d notas deixadas aqui — toque para ler</string>
|
||||
</resources>
|
||||
|
||||
@ -390,4 +390,7 @@
|
||||
<string name="verify_success_body">Вы проверили %1$s</string>
|
||||
<string name="verify_success_system_message">проверен %1$s</string>
|
||||
<string name="cd_open_about">Открыть раздел «О приложении»</string>
|
||||
<string name="nearby_notes_reveal">проверить, есть ли здесь заметки</string>
|
||||
<string name="nearby_notes_one">здесь оставлена 1 заметка — нажмите, чтобы прочитать</string>
|
||||
<string name="nearby_notes_many">здесь оставлено заметок: %d — нажмите, чтобы прочитать</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Öppna Om</string>
|
||||
<string name="nearby_notes_reveal">kolla om anteckningar lämnats här</string>
|
||||
<string name="nearby_notes_one">1 anteckning lämnad här — tryck för att läsa</string>
|
||||
<string name="nearby_notes_many">%d anteckningar lämnade här — tryck för att läsa</string>
|
||||
</resources>
|
||||
|
||||
@ -41,4 +41,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">அறிமுகத்தைத் திற</string>
|
||||
<string name="nearby_notes_reveal">இங்கே விடப்பட்ட குறிப்புகள் உள்ளதா எனப் பார்க்கவும்</string>
|
||||
<string name="nearby_notes_one">இங்கே 1 குறிப்பு விடப்பட்டுள்ளது — படிக்க தட்டவும்</string>
|
||||
<string name="nearby_notes_many">இங்கே %d குறிப்புகள் விடப்பட்டுள்ளன — படிக்க தட்டவும்</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">เปิดเกี่ยวกับ</string>
|
||||
<string name="nearby_notes_reveal">ดูว่ามีโน้ตทิ้งไว้ที่นี่หรือไม่</string>
|
||||
<string name="nearby_notes_one">มี 1 โน้ตทิ้งไว้ที่นี่ — แตะเพื่ออ่าน</string>
|
||||
<string name="nearby_notes_many">มี %d โน้ตทิ้งไว้ที่นี่ — แตะเพื่ออ่าน</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Hakkında’yı aç</string>
|
||||
<string name="nearby_notes_reveal">buraya bırakılan notlara bak</string>
|
||||
<string name="nearby_notes_one">buraya 1 not bırakıldı — okumak için dokun</string>
|
||||
<string name="nearby_notes_many">buraya %d not bırakıldı — okumak için dokun</string>
|
||||
</resources>
|
||||
|
||||
@ -41,4 +41,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Відкрити розділ «Про застосунок»</string>
|
||||
<string name="nearby_notes_reveal">перевірити, чи залишено тут нотатки</string>
|
||||
<string name="nearby_notes_one">тут залишено 1 нотатку — торкніться, щоб прочитати</string>
|
||||
<string name="nearby_notes_many">тут залишено %d нотаток — торкніться, щоб прочитати</string>
|
||||
</resources>
|
||||
|
||||
@ -401,4 +401,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">تعارف کھولیں</string>
|
||||
<string name="nearby_notes_reveal">دیکھیں کہ یہاں نوٹ چھوڑے گئے ہیں یا نہیں</string>
|
||||
<string name="nearby_notes_one">یہاں 1 نوٹ چھوڑا گیا ہے — پڑھنے کے لیے تھپتھپائیں</string>
|
||||
<string name="nearby_notes_many">یہاں %d نوٹ چھوڑے گئے ہیں — پڑھنے کے لیے تھپتھپائیں</string>
|
||||
</resources>
|
||||
|
||||
@ -388,4 +388,7 @@
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
<string name="cd_open_about">Mở phần Giới thiệu</string>
|
||||
<string name="nearby_notes_reveal">kiểm tra ghi chú để lại ở đây</string>
|
||||
<string name="nearby_notes_one">có 1 ghi chú để lại ở đây — chạm để đọc</string>
|
||||
<string name="nearby_notes_many">có %d ghi chú để lại ở đây — chạm để đọc</string>
|
||||
</resources>
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
<string name="nearby_notes_reveal">查看这里留下的留言</string>
|
||||
<string name="nearby_notes_one">这里留有 1 条留言 — 点按阅读</string>
|
||||
<string name="nearby_notes_many">这里留有 %d 条留言 — 点按阅读</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -53,5 +53,7 @@
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
<string name="nearby_notes_reveal">查看這裡留下的留言</string>
|
||||
<string name="nearby_notes_one">這裡留有 1 則留言 — 點按閱讀</string>
|
||||
<string name="nearby_notes_many">這裡留有 %d 則留言 — 點按閱讀</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -413,4 +413,7 @@
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
<string name="cd_open_about">打开“关于”</string>
|
||||
<string name="nearby_notes_reveal">查看这里留下的留言</string>
|
||||
<string name="nearby_notes_one">这里留有 1 条留言 — 点按阅读</string>
|
||||
<string name="nearby_notes_many">这里留有 %d 条留言 — 点按阅读</string>
|
||||
</resources>
|
||||
|
||||
@ -234,6 +234,10 @@
|
||||
<string name="location_level_region">region</string>
|
||||
|
||||
<!-- Location notes sheet -->
|
||||
<string name="nearby_notes_reveal">check for notes left here</string>
|
||||
<string name="nearby_notes_one">1 note left here — tap to read</string>
|
||||
<!-- Explicit one/many copy mirrors iOS across locales without CLDR quantity gaps. -->
|
||||
<string name="nearby_notes_many" tools:ignore="PluralsCandidate">%d notes left here — tap to read</string>
|
||||
<plurals name="location_notes_title">
|
||||
<item quantity="one">#%1$s ± 1 • %2$d note</item>
|
||||
<item quantity="other">#%1$s ± 1 • %2$d notes</item>
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import com.bitchat.android.geohash.GeohashChannel
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NearbyNotesControllerTest {
|
||||
private val subscriptions = mutableListOf<String>()
|
||||
private var unsubscribeCount = 0
|
||||
|
||||
private fun controller() = NearbyNotesController(
|
||||
subscribe = subscriptions::add,
|
||||
unsubscribe = { unsubscribeCount += 1 },
|
||||
)
|
||||
|
||||
private fun foregroundController() = controller().also {
|
||||
it.updateAppForeground(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `active mesh timeline does not subscribe before explicit reveal`() {
|
||||
val controller = foregroundController()
|
||||
|
||||
controller.updateAvailability(
|
||||
locationEnabled = true,
|
||||
locationAuthorized = true,
|
||||
buildingGeohash = "u4pruydq",
|
||||
)
|
||||
controller.activate()
|
||||
|
||||
assertTrue(controller.offersRevealHint())
|
||||
assertTrue(subscriptions.isEmpty())
|
||||
|
||||
controller.reveal()
|
||||
|
||||
assertFalse(controller.offersRevealHint())
|
||||
assertEquals(listOf("u4pruydq"), subscriptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reveal remains dormant until a nearby notes surface is active`() {
|
||||
val controller = foregroundController()
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
|
||||
controller.reveal()
|
||||
|
||||
assertTrue(subscriptions.isEmpty())
|
||||
|
||||
controller.activate()
|
||||
|
||||
assertEquals(listOf("u4pruydq"), subscriptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `last deactivate unsubscribes exactly once`() {
|
||||
val controller = foregroundController()
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
controller.reveal()
|
||||
controller.activate()
|
||||
controller.activate()
|
||||
|
||||
controller.deactivate()
|
||||
assertEquals(0, unsubscribeCount)
|
||||
|
||||
controller.deactivate()
|
||||
controller.deactivate()
|
||||
|
||||
assertEquals(1, unsubscribeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `backgrounding closes the subscription and foregrounding restores it`() {
|
||||
val controller = foregroundController()
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
controller.activate()
|
||||
controller.reveal()
|
||||
|
||||
controller.updateAppForeground(false)
|
||||
|
||||
assertEquals(1, unsubscribeCount)
|
||||
assertTrue(controller.revealed.value)
|
||||
|
||||
controller.updateAppForeground(false)
|
||||
assertEquals(1, unsubscribeCount)
|
||||
|
||||
controller.updateAppForeground(true)
|
||||
assertEquals(listOf("u4pruydq", "u4pruydq"), subscriptions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disable and permission revocation close the live subscription`() {
|
||||
val controller = foregroundController()
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
controller.activate()
|
||||
controller.reveal()
|
||||
|
||||
controller.updateAvailability(false, true, "u4pruydq")
|
||||
assertEquals(1, unsubscribeCount)
|
||||
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
assertEquals(listOf("u4pruydq", "u4pruydq"), subscriptions)
|
||||
|
||||
controller.updateAvailability(true, false, "u4pruydq")
|
||||
assertEquals(2, unsubscribeCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `moving building cells releases old subscription before retargeting`() {
|
||||
val events = mutableListOf<String>()
|
||||
val controller = NearbyNotesController(
|
||||
subscribe = { events += "subscribe:$it" },
|
||||
unsubscribe = { events += "unsubscribe" },
|
||||
)
|
||||
controller.updateAppForeground(true)
|
||||
controller.updateAvailability(true, true, "u4pruydq")
|
||||
controller.activate()
|
||||
controller.reveal()
|
||||
|
||||
controller.updateAvailability(true, true, "u4pruydr")
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"subscribe:u4pruydq",
|
||||
"unsubscribe",
|
||||
"subscribe:u4pruydr",
|
||||
),
|
||||
events,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `building sampling is excluded until reveal while bookmarks remain eligible`() {
|
||||
val channels = listOf(
|
||||
GeohashChannel(GeohashChannelLevel.BUILDING, "u4pruydq"),
|
||||
GeohashChannel(GeohashChannelLevel.BLOCK, "u4pruyd"),
|
||||
GeohashChannel(GeohashChannelLevel.CITY, "u4pru"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("u4pruyd", "u4pru", "saved123"),
|
||||
geohashesForSampling(
|
||||
availableChannels = channels,
|
||||
bookmarks = listOf("saved123"),
|
||||
notesRevealed = false,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("u4pruydq", "u4pruyd", "u4pru", "saved123"),
|
||||
geohashesForSampling(
|
||||
availableChannels = channels,
|
||||
bookmarks = listOf("saved123"),
|
||||
notesRevealed = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user