mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 08:25:22 +00:00
Move PrivateChatSheet hosting into ChatDialogs and unify sheet state (#586)
* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025 * Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025 * refactor: new close button like ios(but not liquid glass) * Automated update of relay data - Sun Oct 5 06:20:09 UTC 2025 * Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025 * Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025 * Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025 * Automated update of relay data - Sun Nov 2 06:22:16 UTC 2025 * Automated update of relay data - Sun Nov 9 06:21:43 UTC 2025 * Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025 * Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025 * Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025 * Automated update of relay data - Sun Dec 7 06:22:59 UTC 2025 * Automated update of relay data - Sun Dec 14 06:24:33 UTC 2025 * Automated update of relay data - Sun Dec 21 06:24:49 UTC 2025 * Automated update of relay data - Sun Dec 28 06:25:38 UTC 2025 * Automated update of relay data - Sun Jan 4 06:26:28 UTC 2026 * Automated update of relay data - Sun Jan 11 06:26:19 UTC 2026 * feat: Show private chat in sheet from notification * Refactor: Hoist private chat sheet state to ChatViewModel * remove icon * remove old bottom sheet --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
co-authored by
GitHub Action
callebtc
parent
829fb52a04
commit
94b28b2107
@@ -774,8 +774,9 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
|
Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
|
||||||
|
|
||||||
// Open the private chat with this peer
|
// Open the private chat sheet with this peer
|
||||||
chatViewModel.startPrivateChat(peerID)
|
chatViewModel.showMeshPeerList()
|
||||||
|
chatViewModel.showPrivateChatSheet(peerID)
|
||||||
|
|
||||||
// Clear notifications for this sender since user is now viewing the chat
|
// Clear notifications for this sender since user is now viewing the chat
|
||||||
chatViewModel.clearNotificationsForSender(peerID)
|
chatViewModel.clearNotificationsForSender(peerID)
|
||||||
|
|||||||
@@ -37,22 +37,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reactive helper to compute favorite state from fingerprint mapping
|
|
||||||
* This eliminates the need for static isFavorite parameters and makes
|
|
||||||
* the UI reactive to fingerprint manager changes
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun isFavoriteReactive(
|
|
||||||
peerID: String,
|
|
||||||
peerFingerprints: Map<String, String>,
|
|
||||||
favoritePeers: Set<String>
|
|
||||||
): Boolean {
|
|
||||||
return remember(peerID, peerFingerprints, favoritePeers) {
|
|
||||||
val fingerprint = peerFingerprints[peerID]
|
|
||||||
fingerprint != null && favoritePeers.contains(fingerprint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TorStatusDot(
|
fun TorStatusDot(
|
||||||
@@ -246,39 +231,6 @@ fun ChatHeaderContent(
|
|||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
when {
|
when {
|
||||||
selectedPrivatePeer != null -> {
|
|
||||||
// Private chat header - Fully reactive state tracking
|
|
||||||
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
|
||||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
|
||||||
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
|
||||||
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
// Reactive favorite computation - no more static lookups!
|
|
||||||
val isFavorite = isFavoriteReactive(
|
|
||||||
peerID = selectedPrivatePeer,
|
|
||||||
peerFingerprints = peerFingerprints,
|
|
||||||
favoritePeers = favoritePeers
|
|
||||||
)
|
|
||||||
val sessionState = peerSessionStates[selectedPrivatePeer]
|
|
||||||
|
|
||||||
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, isFav=$isFavorite, sessionState=$sessionState")
|
|
||||||
|
|
||||||
// Pass geohash context and people for NIP-17 chat title formatting
|
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
|
||||||
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
PrivateChatHeader(
|
|
||||||
peerID = selectedPrivatePeer,
|
|
||||||
peerNicknames = peerNicknames,
|
|
||||||
isFavorite = isFavorite,
|
|
||||||
sessionState = sessionState,
|
|
||||||
selectedLocationChannel = selectedLocationChannel,
|
|
||||||
geohashPeople = geohashPeople,
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) },
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
currentChannel != null -> {
|
currentChannel != null -> {
|
||||||
// Channel header
|
// Channel header
|
||||||
ChannelHeader(
|
ChannelHeader(
|
||||||
@@ -304,168 +256,7 @@ fun ChatHeaderContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PrivateChatHeader(
|
|
||||||
peerID: String,
|
|
||||||
peerNicknames: Map<String, String>,
|
|
||||||
isFavorite: Boolean,
|
|
||||||
sessionState: String?,
|
|
||||||
selectedLocationChannel: com.bitchat.android.geohash.ChannelID?,
|
|
||||||
geohashPeople: List<GeoPerson>,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
onToggleFavorite: () -> Unit,
|
|
||||||
viewModel: ChatViewModel
|
|
||||||
) {
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
val isNostrDM = peerID.startsWith("nostr_") || peerID.startsWith("nostr:")
|
|
||||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
|
||||||
val isVerified = remember(peerID, verifiedFingerprints) {
|
|
||||||
viewModel.isPeerVerified(peerID, verifiedFingerprints)
|
|
||||||
}
|
|
||||||
// Determine mutual favorite state for this peer (supports mesh ephemeral 16-hex via favorites lookup)
|
|
||||||
val isMutualFavorite = remember(peerID, peerNicknames) {
|
|
||||||
try {
|
|
||||||
if (isNostrDM) return@remember false
|
|
||||||
if (peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
|
||||||
val noiseKeyBytes = peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKeyBytes)?.isMutual == true
|
|
||||||
} else if (peerID.length == 16 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(peerID)?.isMutual == true
|
|
||||||
} else false
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute title text: for NIP-17 chats show "#geohash/@username" (iOS parity)
|
|
||||||
val titleText: String = if (isNostrDM) {
|
|
||||||
// For geohash DMs, get the actual source geohash and proper display name
|
|
||||||
val (conversationGeohash, baseName) = try {
|
|
||||||
val repoField = com.bitchat.android.ui.GeohashViewModel::class.java.getDeclaredField("repo")
|
|
||||||
repoField.isAccessible = true
|
|
||||||
val repo = repoField.get(viewModel.geohashViewModel) as com.bitchat.android.nostr.GeohashRepository
|
|
||||||
val gh = repo.getConversationGeohash(peerID) ?: "geohash"
|
|
||||||
val fullPubkey = com.bitchat.android.nostr.GeohashAliasRegistry.get(peerID) ?: ""
|
|
||||||
val displayName = if (fullPubkey.isNotEmpty()) {
|
|
||||||
repo.displayNameForGeohashConversation(fullPubkey, gh)
|
|
||||||
} else {
|
|
||||||
peerNicknames[peerID] ?: "unknown"
|
|
||||||
}
|
|
||||||
Pair(gh, displayName)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Pair("geohash", peerNicknames[peerID] ?: "unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
"#$conversationGeohash/@$baseName"
|
|
||||||
} else {
|
|
||||||
// Prefer live mesh nickname; fallback to favorites nickname (supports 16-hex), finally short key
|
|
||||||
peerNicknames[peerID] ?: run {
|
|
||||||
val titleFromFavorites = try {
|
|
||||||
if (peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
|
||||||
val noiseKeyBytes = peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKeyBytes)?.peerNickname
|
|
||||||
} else if (peerID.length == 16 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
|
||||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(peerID)?.peerNickname
|
|
||||||
} else null
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
titleFromFavorites ?: peerID.take(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
// Back button - positioned all the way to the left with minimal margin
|
|
||||||
Button(
|
|
||||||
onClick = onBackClick,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = colorScheme.primary
|
|
||||||
),
|
|
||||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.offset(x = (-8).dp) // Move even further left to minimize margin
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.back),
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.chat_back),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title - perfectly centered regardless of other elements
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = titleText,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = Color(0xFFFF9500) // Orange
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
// Show a globe when chatting via Nostr alias, or when mesh session not established but mutual favorite exists
|
|
||||||
val showGlobe = isNostrDM || (sessionState != "established" && isMutualFavorite)
|
|
||||||
val securityModifier = if (!isNostrDM) {
|
|
||||||
Modifier.clickable { viewModel.showSecurityVerificationSheet() }
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showGlobe) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Public,
|
|
||||||
contentDescription = stringResource(R.string.cd_nostr_reachable),
|
|
||||||
modifier = Modifier.size(14.dp).then(securityModifier),
|
|
||||||
tint = Color(0xFF9B59B6) // Purple like iOS
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
NoiseSessionIcon(
|
|
||||||
sessionState = sessionState,
|
|
||||||
modifier = Modifier.size(14.dp).then(securityModifier)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVerified) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Verified,
|
|
||||||
contentDescription = stringResource(R.string.verify_title),
|
|
||||||
modifier = Modifier.size(14.dp).then(securityModifier),
|
|
||||||
tint = Color(0xFF32D74B)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favorite button - positioned on the right
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
|
|
||||||
onToggleFavorite()
|
|
||||||
},
|
|
||||||
modifier = Modifier.align(Alignment.CenterEnd)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
|
||||||
contentDescription = if (isFavorite) stringResource(R.string.cd_remove_favorite) else stringResource(R.string.cd_add_favorite),
|
|
||||||
modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon
|
|
||||||
tint = if (isFavorite) Color(0xFFFFD700) else Color(0x87878700) // Yellow or grey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChannelHeader(
|
private fun ChannelHeader(
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
|
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
|
||||||
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
|
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
|
||||||
val showMeshPeerListSheet by viewModel.showMeshPeerList.collectAsStateWithLifecycle()
|
val showMeshPeerListSheet by viewModel.showMeshPeerList.collectAsStateWithLifecycle()
|
||||||
|
val privateChatSheetPeer by viewModel.privateChatSheetPeer.collectAsStateWithLifecycle()
|
||||||
val showVerificationSheet by viewModel.showVerificationSheet.collectAsStateWithLifecycle()
|
val showVerificationSheet by viewModel.showVerificationSheet.collectAsStateWithLifecycle()
|
||||||
val showSecurityVerificationSheet by viewModel.showSecurityVerificationSheet.collectAsStateWithLifecycle()
|
val showSecurityVerificationSheet by viewModel.showSecurityVerificationSheet.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -86,8 +87,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Determine what messages to show based on current context (unified timelines)
|
// Determine what messages to show based on current context (unified timelines)
|
||||||
|
// Legacy private chat timeline removed - private chats now exclusively use PrivateChatSheet
|
||||||
val displayMessages = when {
|
val displayMessages = when {
|
||||||
selectedPrivatePeer != null -> privateChats[selectedPrivatePeer] ?: emptyList()
|
|
||||||
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
|
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
|
||||||
else -> {
|
else -> {
|
||||||
val locationChannel = selectedLocationChannel
|
val locationChannel = selectedLocationChannel
|
||||||
@@ -102,7 +103,6 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
|
|
||||||
// Determine whether to show media buttons (only hide in geohash location chats)
|
// Determine whether to show media buttons (only hide in geohash location chats)
|
||||||
val showMediaButtons = when {
|
val showMediaButtons = when {
|
||||||
selectedPrivatePeer != null -> true
|
|
||||||
currentChannel != null -> true
|
currentChannel != null -> true
|
||||||
else -> selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location
|
else -> selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
selection = TextRange(mentionText.length)
|
selection = TextRange(mentionText.length)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectedPrivatePeer = selectedPrivatePeer,
|
selectedPrivatePeer = null,
|
||||||
currentChannel = currentChannel,
|
currentChannel = currentChannel,
|
||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
@@ -243,7 +243,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
// Floating header - positioned absolutely at top, ignores keyboard
|
// Floating header - positioned absolutely at top, ignores keyboard
|
||||||
ChatFloatingHeader(
|
ChatFloatingHeader(
|
||||||
headerHeight = headerHeight,
|
headerHeight = headerHeight,
|
||||||
selectedPrivatePeer = selectedPrivatePeer,
|
selectedPrivatePeer = null,
|
||||||
currentChannel = currentChannel,
|
currentChannel = currentChannel,
|
||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -490,6 +490,8 @@ private fun ChatDialogs(
|
|||||||
showMeshPeerListSheet: Boolean,
|
showMeshPeerListSheet: Boolean,
|
||||||
onMeshPeerListDismiss: () -> Unit,
|
onMeshPeerListDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val privateChatSheetPeer by viewModel.privateChatSheetPeer.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Password dialog
|
// Password dialog
|
||||||
PasswordPromptDialog(
|
PasswordPromptDialog(
|
||||||
show = showPasswordDialog,
|
show = showPasswordDialog,
|
||||||
@@ -570,4 +572,16 @@ private fun ChatDialogs(
|
|||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (privateChatSheetPeer != null) {
|
||||||
|
PrivateChatSheet(
|
||||||
|
isPresented = true,
|
||||||
|
peerID = privateChatSheetPeer!!,
|
||||||
|
viewModel = viewModel,
|
||||||
|
onDismiss = {
|
||||||
|
viewModel.hidePrivateChatSheet()
|
||||||
|
viewModel.endPrivateChat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ class ChatState(
|
|||||||
private val _showMeshPeerList = MutableStateFlow(false)
|
private val _showMeshPeerList = MutableStateFlow(false)
|
||||||
val showMeshPeerList: StateFlow<Boolean> = _showMeshPeerList.asStateFlow()
|
val showMeshPeerList: StateFlow<Boolean> = _showMeshPeerList.asStateFlow()
|
||||||
|
|
||||||
|
private val _privateChatSheetPeer = MutableStateFlow<String?>(null)
|
||||||
|
val privateChatSheetPeer: StateFlow<String?> = _privateChatSheetPeer.asStateFlow()
|
||||||
|
|
||||||
private val _showVerificationSheet = MutableStateFlow(false)
|
private val _showVerificationSheet = MutableStateFlow(false)
|
||||||
val showVerificationSheet: StateFlow<Boolean> = _showVerificationSheet.asStateFlow()
|
val showVerificationSheet: StateFlow<Boolean> = _showVerificationSheet.asStateFlow()
|
||||||
|
|
||||||
@@ -188,6 +191,7 @@ class ChatState(
|
|||||||
fun getGeohashPeopleValue() = _geohashPeople.value
|
fun getGeohashPeopleValue() = _geohashPeople.value
|
||||||
|
|
||||||
fun getShowMeshPeerListValue() = _showMeshPeerList.value
|
fun getShowMeshPeerListValue() = _showMeshPeerList.value
|
||||||
|
fun getPrivateChatSheetPeerValue() = _privateChatSheetPeer.value
|
||||||
|
|
||||||
fun getTeleportedGeoValue() = _teleportedGeo.value
|
fun getTeleportedGeoValue() = _teleportedGeo.value
|
||||||
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value
|
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value
|
||||||
@@ -337,4 +341,8 @@ class ChatState(
|
|||||||
fun setShowMeshPeerList(show: Boolean) {
|
fun setShowMeshPeerList(show: Boolean) {
|
||||||
_showMeshPeerList.value = show
|
_showMeshPeerList.value = show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPrivateChatSheetPeer(peerID: String?) {
|
||||||
|
_privateChatSheetPeer.value = peerID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class ChatViewModel(
|
|||||||
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
|
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
|
||||||
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
|
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
|
||||||
val showMeshPeerList: StateFlow<Boolean> = state.showMeshPeerList
|
val showMeshPeerList: StateFlow<Boolean> = state.showMeshPeerList
|
||||||
|
val privateChatSheetPeer: StateFlow<String?> = state.privateChatSheetPeer
|
||||||
val showVerificationSheet: StateFlow<Boolean> = state.showVerificationSheet
|
val showVerificationSheet: StateFlow<Boolean> = state.showVerificationSheet
|
||||||
val showSecurityVerificationSheet: StateFlow<Boolean> = state.showSecurityVerificationSheet
|
val showSecurityVerificationSheet: StateFlow<Boolean> = state.showSecurityVerificationSheet
|
||||||
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||||
@@ -403,6 +404,8 @@ class ChatViewModel(
|
|||||||
setCurrentPrivateChatPeer(null)
|
setCurrentPrivateChatPeer(null)
|
||||||
// Clear mesh mention notifications since user is now back in mesh chat
|
// Clear mesh mention notifications since user is now back in mesh chat
|
||||||
clearMeshMentionNotifications()
|
clearMeshMentionNotifications()
|
||||||
|
// Ensure sheet is hidden
|
||||||
|
hidePrivateChatSheet()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Open Latest Unread Private Chat
|
// MARK: - Open Latest Unread Private Chat
|
||||||
@@ -451,7 +454,7 @@ class ChatViewModel(
|
|||||||
canonical ?: targetKey
|
canonical ?: targetKey
|
||||||
}
|
}
|
||||||
|
|
||||||
startPrivateChat(openPeer)
|
showPrivateChatSheet(openPeer)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "openLatestUnreadPrivateChat failed: ${e.message}")
|
Log.w(TAG, "openLatestUnreadPrivateChat failed: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -796,6 +799,18 @@ class ChatViewModel(
|
|||||||
|
|
||||||
fun hideMeshPeerList() {
|
fun hideMeshPeerList() {
|
||||||
state.setShowMeshPeerList(false)
|
state.setShowMeshPeerList(false)
|
||||||
|
if (state.getPrivateChatSheetPeerValue() != null) {
|
||||||
|
endPrivateChat()
|
||||||
|
}
|
||||||
|
hidePrivateChatSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showPrivateChatSheet(peerID: String) {
|
||||||
|
state.setPrivateChatSheetPeer(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hidePrivateChatSheet() {
|
||||||
|
state.setPrivateChatSheetPeer(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
||||||
@@ -1006,7 +1021,7 @@ class ChatViewModel(
|
|||||||
*/
|
*/
|
||||||
fun startGeohashDM(pubkeyHex: String) {
|
fun startGeohashDM(pubkeyHex: String) {
|
||||||
geohashViewModel.startGeohashDM(pubkeyHex) { convKey ->
|
geohashViewModel.startGeohashDM(pubkeyHex) { convKey ->
|
||||||
startPrivateChat(convKey)
|
showPrivateChatSheet(convKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,7 +1064,7 @@ class ChatViewModel(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
// Exit private chat
|
// Exit private chat
|
||||||
state.getSelectedPrivateChatPeerValue() != null -> {
|
state.getSelectedPrivateChatPeerValue() != null || state.getPrivateChatSheetPeerValue() != null -> {
|
||||||
endPrivateChat()
|
endPrivateChat()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,6 @@ fun MeshPeerListSheet(
|
|||||||
val peerRSSI by viewModel.peerRSSI.collectAsStateWithLifecycle()
|
val peerRSSI by viewModel.peerRSSI.collectAsStateWithLifecycle()
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Track nested private chat sheet state
|
|
||||||
var showPrivateChatSheet by remember { mutableStateOf(false) }
|
|
||||||
var privateChatPeerID by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
// Bottom sheet state
|
// Bottom sheet state
|
||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
@@ -125,8 +121,7 @@ fun MeshPeerListSheet(
|
|||||||
val peerID =
|
val peerID =
|
||||||
peerNicknames.entries.firstOrNull { it.value == peerName }?.key
|
peerNicknames.entries.firstOrNull { it.value == peerName }?.key
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
privateChatPeerID = peerID
|
viewModel.showPrivateChatSheet(peerID)
|
||||||
showPrivateChatSheet = true
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular channel switch
|
// Regular channel switch
|
||||||
@@ -164,9 +159,7 @@ fun MeshPeerListSheet(
|
|||||||
selectedPrivatePeer = selectedPrivatePeer,
|
selectedPrivatePeer = selectedPrivatePeer,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onPrivateChatStart = { peerID ->
|
onPrivateChatStart = { peerID ->
|
||||||
viewModel.startPrivateChat(peerID)
|
viewModel.showPrivateChatSheet(peerID)
|
||||||
privateChatPeerID = peerID
|
|
||||||
showPrivateChatSheet = true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -223,19 +216,6 @@ fun MeshPeerListSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested Private Chat Sheet (iOS-style)
|
|
||||||
if (showPrivateChatSheet && privateChatPeerID != null) {
|
|
||||||
PrivateChatSheet(
|
|
||||||
isPresented = showPrivateChatSheet,
|
|
||||||
peerID = privateChatPeerID!!,
|
|
||||||
viewModel = viewModel,
|
|
||||||
onDismiss = {
|
|
||||||
showPrivateChatSheet = false
|
|
||||||
privateChatPeerID = null
|
|
||||||
viewModel.endPrivateChat()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,16 +275,6 @@ private fun ChannelRow(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Selection indicator
|
|
||||||
if (isSelected) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = stringResource(R.string.cd_selected),
|
|
||||||
tint = Color(0xFF32D74B), // iOS green
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leave channel button
|
// Leave channel button
|
||||||
CloseButton(
|
CloseButton(
|
||||||
onClick = onLeaveChannel,
|
onClick = onLeaveChannel,
|
||||||
@@ -715,16 +685,6 @@ private fun PeerItem(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Selection indicator
|
|
||||||
if (isSelected) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = stringResource(R.string.cd_selected),
|
|
||||||
tint = Color(0xFF32D74B), // iOS green
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favorite star with proper filled/outlined states
|
// Favorite star with proper filled/outlined states
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleFavorite,
|
onClick = onToggleFavorite,
|
||||||
@@ -802,7 +762,7 @@ private fun convertRSSIToSignalStrength(rssi: Int?): Int {
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PrivateChatSheet(
|
fun PrivateChatSheet(
|
||||||
isPresented: Boolean,
|
isPresented: Boolean,
|
||||||
peerID: String,
|
peerID: String,
|
||||||
viewModel: ChatViewModel,
|
viewModel: ChatViewModel,
|
||||||
|
|||||||
Reference in New Issue
Block a user