mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:45:22 +00:00
@@ -1,6 +1,7 @@
|
|||||||
package com.bitchat.android
|
package com.bitchat.android
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -44,6 +45,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Request necessary permissions
|
// Request necessary permissions
|
||||||
requestPermissions()
|
requestPermissions()
|
||||||
|
|
||||||
|
// Handle notification intent if app was opened from a notification
|
||||||
|
handleNotificationIntent(intent)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
BitchatTheme {
|
BitchatTheme {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -56,18 +60,49 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
// Handle notification intents when app is already running
|
||||||
|
intent?.let { handleNotificationIntent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
// Notify that app is in foreground for power optimization
|
// Notify that app is in foreground for power optimization and notifications
|
||||||
chatViewModel.setAppBackgroundState(false)
|
chatViewModel.setAppBackgroundState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
// Notify that app is in background for power optimization
|
// Notify that app is in background for power optimization and notifications
|
||||||
chatViewModel.setAppBackgroundState(true)
|
chatViewModel.setAppBackgroundState(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle intents from notification clicks - open specific private chat
|
||||||
|
*/
|
||||||
|
private fun handleNotificationIntent(intent: Intent) {
|
||||||
|
val shouldOpenPrivateChat = intent.getBooleanExtra(
|
||||||
|
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_PRIVATE_CHAT,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shouldOpenPrivateChat) {
|
||||||
|
val peerID = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_PEER_ID)
|
||||||
|
val senderNickname = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME)
|
||||||
|
|
||||||
|
if (peerID != null) {
|
||||||
|
android.util.Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
|
||||||
|
|
||||||
|
// Open the private chat with this peer
|
||||||
|
chatViewModel.startPrivateChat(peerID)
|
||||||
|
|
||||||
|
// Clear notifications for this sender since user is now viewing the chat
|
||||||
|
chatViewModel.clearNotificationsForSender(peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun requestPermissions() {
|
private fun requestPermissions() {
|
||||||
val permissions = mutableListOf<String>()
|
val permissions = mutableListOf<String>()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -78,24 +79,31 @@ fun PeerCounter(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier.clickable { onClick() }
|
modifier = modifier.clickable { onClick() }.padding(end = 8.dp) // Added right margin to match "bitchat" logo spacing
|
||||||
) {
|
) {
|
||||||
if (hasUnreadChannels.values.any { it > 0 }) {
|
if (hasUnreadChannels.values.any { it > 0 }) {
|
||||||
Text(
|
// Channel icon in a Box to ensure consistent size with other icons
|
||||||
text = "#",
|
Box(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
modifier = Modifier.size(16.dp),
|
||||||
color = Color(0xFF0080FF),
|
contentAlignment = Alignment.Center
|
||||||
fontSize = 16.sp
|
) {
|
||||||
)
|
Text(
|
||||||
|
text = "#",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color(0xFF0080FF),
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUnreadPrivateMessages.isNotEmpty()) {
|
if (hasUnreadPrivateMessages.isNotEmpty()) {
|
||||||
Text(
|
// Filled mail icon to match sidebar style
|
||||||
text = "✉",
|
Icon(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
imageVector = Icons.Filled.Email,
|
||||||
color = Color(0xFFFF8C00),
|
contentDescription = "Unread private messages",
|
||||||
fontSize = 16.sp
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
}
|
}
|
||||||
@@ -198,18 +206,42 @@ private fun PrivateChatHeader(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBackClick) {
|
// Fixed: Make back button wider to prevent text cropping
|
||||||
Text(
|
Button(
|
||||||
text = "← back",
|
onClick = onBackClick,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
colors = ButtonDefaults.buttonColors(
|
||||||
color = colorScheme.primary
|
containerColor = Color.Transparent,
|
||||||
)
|
contentColor = colorScheme.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "back",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("🔒", fontSize = 16.sp) // Slightly larger
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Lock,
|
||||||
|
contentDescription = "Private chat",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = peerNickname,
|
text = peerNickname,
|
||||||
@@ -220,12 +252,13 @@ private fun PrivateChatHeader(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
// Favorite button
|
// Favorite button with proper filled/outlined star
|
||||||
IconButton(onClick = onToggleFavorite) {
|
IconButton(onClick = onToggleFavorite) {
|
||||||
Text(
|
Icon(
|
||||||
text = if (isFavorite) "★" else "☆",
|
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
||||||
color = if (isFavorite) Color.Yellow else colorScheme.primary,
|
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
||||||
fontSize = 18.sp // Larger icon
|
modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon
|
||||||
|
tint = if (isFavorite) Color(0xFFFFD700) else Color(0xFF4CAF50) // Yellow for filled, green for outlined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,11 +279,22 @@ private fun ChannelHeader(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onBackClick) {
|
IconButton(onClick = onBackClick) {
|
||||||
Text(
|
Row(
|
||||||
text = "← back",
|
verticalAlignment = Alignment.CenterVertically
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
) {
|
||||||
color = colorScheme.primary
|
Icon(
|
||||||
)
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "back",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ class ChatState {
|
|||||||
private val _commandSuggestions = MutableLiveData<List<CommandSuggestion>>(emptyList())
|
private val _commandSuggestions = MutableLiveData<List<CommandSuggestion>>(emptyList())
|
||||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = _commandSuggestions
|
val commandSuggestions: LiveData<List<CommandSuggestion>> = _commandSuggestions
|
||||||
|
|
||||||
|
// Favorites
|
||||||
|
private val _favoritePeers = MutableLiveData<Set<String>>(emptySet())
|
||||||
|
val favoritePeers: LiveData<Set<String>> = _favoritePeers
|
||||||
|
|
||||||
|
val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
|
||||||
|
|
||||||
// Unread state computed properties
|
// Unread state computed properties
|
||||||
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||||
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||||
@@ -110,6 +116,7 @@ class ChatState {
|
|||||||
fun getShowSidebarValue() = _showSidebar.value ?: false
|
fun getShowSidebarValue() = _showSidebar.value ?: false
|
||||||
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
||||||
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
||||||
|
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
|
||||||
|
|
||||||
// Setters for state updates
|
// Setters for state updates
|
||||||
fun setMessages(messages: List<BitchatMessage>) {
|
fun setMessages(messages: List<BitchatMessage>) {
|
||||||
@@ -179,4 +186,9 @@ class ChatState {
|
|||||||
fun setCommandSuggestions(suggestions: List<CommandSuggestion>) {
|
fun setCommandSuggestions(suggestions: List<CommandSuggestion>) {
|
||||||
_commandSuggestions.value = suggestions
|
_commandSuggestions.value = suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFavoritePeers(favorites: Set<String>) {
|
||||||
|
_favoritePeers.value = favorites
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
private val dataManager = DataManager(context)
|
private val dataManager = DataManager(context)
|
||||||
private val messageManager = MessageManager(state)
|
private val messageManager = MessageManager(state)
|
||||||
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
|
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
|
||||||
private val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
|
val privateChatManager = PrivateChatManager(state, messageManager, dataManager)
|
||||||
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
|
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
|
||||||
|
private val notificationManager = NotificationManager(application.applicationContext)
|
||||||
|
|
||||||
// Delegate handler for mesh callbacks
|
// Delegate handler for mesh callbacks
|
||||||
private val meshDelegateHandler = MeshDelegateHandler(
|
private val meshDelegateHandler = MeshDelegateHandler(
|
||||||
@@ -42,6 +43,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
messageManager = messageManager,
|
messageManager = messageManager,
|
||||||
channelManager = channelManager,
|
channelManager = channelManager,
|
||||||
privateChatManager = privateChatManager,
|
privateChatManager = privateChatManager,
|
||||||
|
notificationManager = notificationManager,
|
||||||
coroutineScope = viewModelScope,
|
coroutineScope = viewModelScope,
|
||||||
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) },
|
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) },
|
||||||
getMyPeerID = { meshService.myPeerID }
|
getMyPeerID = { meshService.myPeerID }
|
||||||
@@ -67,6 +69,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
||||||
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
||||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
||||||
|
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
||||||
|
|
||||||
init {
|
init {
|
||||||
meshService.delegate = this
|
meshService.delegate = this
|
||||||
@@ -94,6 +97,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
|
|
||||||
// Load other data
|
// Load other data
|
||||||
dataManager.loadFavorites()
|
dataManager.loadFavorites()
|
||||||
|
state.setFavoritePeers(dataManager.favoritePeers)
|
||||||
dataManager.loadBlockedUsers()
|
dataManager.loadBlockedUsers()
|
||||||
|
|
||||||
// Start mesh service
|
// Start mesh service
|
||||||
@@ -145,11 +149,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
// MARK: - Private Chat Management (delegated)
|
// MARK: - Private Chat Management (delegated)
|
||||||
|
|
||||||
fun startPrivateChat(peerID: String) {
|
fun startPrivateChat(peerID: String) {
|
||||||
privateChatManager.startPrivateChat(peerID, meshService)
|
val success = privateChatManager.startPrivateChat(peerID, meshService)
|
||||||
|
if (success) {
|
||||||
|
// Notify notification manager about current private chat
|
||||||
|
setCurrentPrivateChatPeer(peerID)
|
||||||
|
// Clear notifications for this sender since user is now viewing the chat
|
||||||
|
clearNotificationsForSender(peerID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endPrivateChat() {
|
fun endPrivateChat() {
|
||||||
privateChatManager.endPrivateChat()
|
privateChatManager.endPrivateChat()
|
||||||
|
// Notify notification manager that no private chat is active
|
||||||
|
setCurrentPrivateChatPeer(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Message Sending
|
// MARK: - Message Sending
|
||||||
@@ -262,6 +274,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
fun setAppBackgroundState(inBackground: Boolean) {
|
fun setAppBackgroundState(inBackground: Boolean) {
|
||||||
// Forward to connection manager for power optimization
|
// Forward to connection manager for power optimization
|
||||||
meshService.connectionManager.setAppBackgroundState(inBackground)
|
meshService.connectionManager.setAppBackgroundState(inBackground)
|
||||||
|
|
||||||
|
// Forward to notification manager for notification logic
|
||||||
|
notificationManager.setAppBackgroundState(inBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentPrivateChatPeer(peerID: String?) {
|
||||||
|
// Update notification manager with current private chat peer
|
||||||
|
notificationManager.setCurrentPrivateChatPeer(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearNotificationsForSender(peerID: String) {
|
||||||
|
// Clear notifications when user opens a chat
|
||||||
|
notificationManager.clearNotificationsForSender(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Command Autocomplete (delegated)
|
// MARK: - Command Autocomplete (delegated)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -42,11 +44,11 @@ fun MessageInput(
|
|||||||
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
|
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Prompt
|
// Fixed: Remove arrow from private message input
|
||||||
Text(
|
Text(
|
||||||
text = when {
|
text = when {
|
||||||
selectedPrivatePeer != null -> "<@$nickname> →"
|
selectedPrivatePeer != null -> "<@$nickname>" // Removed arrow for private
|
||||||
currentChannel != null -> "<@$nickname> →" // Could show if channel is encrypted
|
currentChannel != null -> "<@$nickname> →" // Keep arrow for channels
|
||||||
else -> "<@$nickname>"
|
else -> "<@$nickname>"
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
|
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
|
||||||
@@ -76,7 +78,7 @@ fun MessageInput(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
||||||
|
|
||||||
// Send button - solid bright green background with thick transparent arrow
|
// Fixed: Make send button orange in private mode to match nickname color
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onSend,
|
onClick = onSend,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
@@ -85,7 +87,10 @@ fun MessageInput(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (colorScheme.background == Color.Black) {
|
color = if (selectedPrivatePeer != null) {
|
||||||
|
// Orange for private messages to match nickname color
|
||||||
|
Color(0xFFFF8C00).copy(alpha = 0.75f)
|
||||||
|
} else if (colorScheme.background == Color.Black) {
|
||||||
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
|
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
|
||||||
} else {
|
} else {
|
||||||
Color(0xFF008000).copy(alpha = 0.75f) // Dark green for light theme
|
Color(0xFF008000).copy(alpha = 0.75f) // Dark green for light theme
|
||||||
@@ -94,17 +99,18 @@ fun MessageInput(
|
|||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Icon(
|
||||||
text = "↑",
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
style = MaterialTheme.typography.titleMedium.copy(
|
contentDescription = "Send message",
|
||||||
fontSize = 20.sp,
|
modifier = Modifier.size(20.dp),
|
||||||
fontWeight = FontWeight.Bold
|
tint = if (selectedPrivatePeer != null) {
|
||||||
),
|
// Black arrow on orange in private mode
|
||||||
color = if (colorScheme.background == Color.Black) {
|
Color.Black
|
||||||
|
} else if (colorScheme.background == Color.Black) {
|
||||||
Color.Black // Black arrow on bright green in dark theme
|
Color.Black // Black arrow on bright green in dark theme
|
||||||
} else {
|
} else {
|
||||||
Color.White // White arrow on dark green in light theme
|
Color.White // White arrow on dark green in light theme
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ class MeshDelegateHandler(
|
|||||||
private val messageManager: MessageManager,
|
private val messageManager: MessageManager,
|
||||||
private val channelManager: ChannelManager,
|
private val channelManager: ChannelManager,
|
||||||
private val privateChatManager: PrivateChatManager,
|
private val privateChatManager: PrivateChatManager,
|
||||||
|
private val notificationManager: NotificationManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val onHapticFeedback: () -> Unit,
|
private val onHapticFeedback: () -> Unit,
|
||||||
private val getMyPeerID: () -> String
|
private val getMyPeerID: () -> String
|
||||||
) : BluetoothMeshDelegate {
|
) : BluetoothMeshDelegate {
|
||||||
|
|
||||||
override fun didReceiveMessage(message: BitchatMessage) {
|
override fun didReceiveMessage(message: BitchatMessage) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
// FIXED: Deduplicate messages from dual connection paths
|
// FIXED: Deduplicate messages from dual connection paths
|
||||||
@@ -45,6 +46,19 @@ class MeshDelegateHandler(
|
|||||||
if (message.isPrivate) {
|
if (message.isPrivate) {
|
||||||
// Private message
|
// Private message
|
||||||
privateChatManager.handleIncomingPrivateMessage(message)
|
privateChatManager.handleIncomingPrivateMessage(message)
|
||||||
|
|
||||||
|
// Show notification with enhanced information - now includes senderPeerID
|
||||||
|
message.senderPeerID?.let { senderPeerID ->
|
||||||
|
if (state.getSelectedPrivateChatPeerValue() != senderPeerID) {
|
||||||
|
// Use nickname if available, fall back to sender or senderPeerID
|
||||||
|
val senderNickname = message.sender.takeIf { it != senderPeerID } ?: senderPeerID
|
||||||
|
notificationManager.showPrivateMessageNotification(
|
||||||
|
senderPeerID = senderPeerID,
|
||||||
|
senderNickname = senderNickname,
|
||||||
|
messageContent = message.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (message.channel != null) {
|
} else if (message.channel != null) {
|
||||||
// Channel message
|
// Channel message
|
||||||
if (state.getJoinedChannelsValue().contains(message.channel)) {
|
if (state.getJoinedChannelsValue().contains(message.channel)) {
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.Person
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import com.bitchat.android.MainActivity
|
||||||
|
import com.bitchat.android.R
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced notification manager for direct messages with production-ready features:
|
||||||
|
* - Notification grouping per sender
|
||||||
|
* - Click handling to open specific DM
|
||||||
|
* - App background state awareness
|
||||||
|
* - Proper notification management and cleanup
|
||||||
|
*/
|
||||||
|
class NotificationManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NotificationManager"
|
||||||
|
private const val CHANNEL_ID = "bitchat_dm_notifications"
|
||||||
|
private const val GROUP_KEY_DM = "bitchat_dm_group"
|
||||||
|
private const val NOTIFICATION_REQUEST_CODE = 1000
|
||||||
|
private const val SUMMARY_NOTIFICATION_ID = 999
|
||||||
|
|
||||||
|
// Intent extras for notification handling
|
||||||
|
const val EXTRA_OPEN_PRIVATE_CHAT = "open_private_chat"
|
||||||
|
const val EXTRA_PEER_ID = "peer_id"
|
||||||
|
const val EXTRA_SENDER_NICKNAME = "sender_nickname"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
private val systemNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
// Track pending notifications per sender to enable grouping
|
||||||
|
private val pendingNotifications = ConcurrentHashMap<String, MutableList<PendingNotification>>()
|
||||||
|
|
||||||
|
// Track app background state
|
||||||
|
@Volatile
|
||||||
|
private var isAppInBackground = false
|
||||||
|
|
||||||
|
// Track current view state
|
||||||
|
@Volatile
|
||||||
|
private var currentPrivateChatPeer: String? = null
|
||||||
|
|
||||||
|
data class PendingNotification(
|
||||||
|
val senderPeerID: String,
|
||||||
|
val senderNickname: String,
|
||||||
|
val messageContent: String,
|
||||||
|
val timestamp: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
createNotificationChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = "Direct Messages"
|
||||||
|
val descriptionText = "Notifications for private messages from other users"
|
||||||
|
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||||
|
description = descriptionText
|
||||||
|
enableVibration(true)
|
||||||
|
setShowBadge(true)
|
||||||
|
}
|
||||||
|
systemNotificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update app background state - notifications should only be shown when app is backgrounded
|
||||||
|
*/
|
||||||
|
fun setAppBackgroundState(inBackground: Boolean) {
|
||||||
|
isAppInBackground = inBackground
|
||||||
|
Log.d(TAG, "App background state changed: $inBackground")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update current private chat peer - affects notification logic
|
||||||
|
*/
|
||||||
|
fun setCurrentPrivateChatPeer(peerID: String?) {
|
||||||
|
currentPrivateChatPeer = peerID
|
||||||
|
Log.d(TAG, "Current private chat peer changed: $peerID")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a notification for a private message with proper grouping and state awareness
|
||||||
|
*/
|
||||||
|
fun showPrivateMessageNotification(senderPeerID: String, senderNickname: String, messageContent: String) {
|
||||||
|
// Only show notifications if app is in background OR user is not viewing this specific chat
|
||||||
|
val shouldNotify = isAppInBackground || currentPrivateChatPeer != senderPeerID
|
||||||
|
|
||||||
|
if (!shouldNotify) {
|
||||||
|
Log.d(TAG, "Skipping notification - app in foreground and viewing chat with $senderNickname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Showing notification for message from $senderNickname (peerID: $senderPeerID)")
|
||||||
|
|
||||||
|
val notification = PendingNotification(
|
||||||
|
senderPeerID = senderPeerID,
|
||||||
|
senderNickname = senderNickname,
|
||||||
|
messageContent = messageContent,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to pending notifications for this sender
|
||||||
|
pendingNotifications.computeIfAbsent(senderPeerID) { mutableListOf() }.add(notification)
|
||||||
|
|
||||||
|
// Create or update notification for this sender
|
||||||
|
showNotificationForSender(senderPeerID)
|
||||||
|
|
||||||
|
// Update summary notification if we have multiple senders
|
||||||
|
if (pendingNotifications.size > 1) {
|
||||||
|
showSummaryNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotificationForSender(senderPeerID: String) {
|
||||||
|
val notifications = pendingNotifications[senderPeerID] ?: return
|
||||||
|
if (notifications.isEmpty()) return
|
||||||
|
|
||||||
|
val latestNotification = notifications.last()
|
||||||
|
val messageCount = notifications.size
|
||||||
|
|
||||||
|
// Create intent to open the specific private chat
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
putExtra(EXTRA_OPEN_PRIVATE_CHAT, true)
|
||||||
|
putExtra(EXTRA_PEER_ID, senderPeerID)
|
||||||
|
putExtra(EXTRA_SENDER_NICKNAME, latestNotification.senderNickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
NOTIFICATION_REQUEST_CODE + senderPeerID.hashCode(),
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create person for better notification styling
|
||||||
|
val person = Person.Builder()
|
||||||
|
.setName(latestNotification.senderNickname)
|
||||||
|
.setKey(senderPeerID)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Build notification content
|
||||||
|
val contentText = if (messageCount == 1) {
|
||||||
|
latestNotification.messageContent
|
||||||
|
} else {
|
||||||
|
"${latestNotification.messageContent} (+${messageCount - 1} more)"
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentTitle = if (messageCount == 1) {
|
||||||
|
latestNotification.senderNickname
|
||||||
|
} else {
|
||||||
|
"${latestNotification.senderNickname} ($messageCount messages)"
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle(contentTitle)
|
||||||
|
.setContentText(contentText)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
|
.addPerson(person)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setWhen(latestNotification.timestamp)
|
||||||
|
|
||||||
|
// Add to notification group if we have multiple senders
|
||||||
|
if (pendingNotifications.size > 1) {
|
||||||
|
builder.setGroup(GROUP_KEY_DM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add style for multiple messages
|
||||||
|
if (messageCount > 1) {
|
||||||
|
val style = NotificationCompat.InboxStyle()
|
||||||
|
.setBigContentTitle(contentTitle)
|
||||||
|
|
||||||
|
// Show last few messages in expanded view
|
||||||
|
notifications.takeLast(5).forEach { notif ->
|
||||||
|
style.addLine(notif.messageContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageCount > 5) {
|
||||||
|
style.setSummaryText("and ${messageCount - 5} more")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setStyle(style)
|
||||||
|
} else {
|
||||||
|
// Single message - use BigTextStyle for long messages
|
||||||
|
builder.setStyle(
|
||||||
|
NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(latestNotification.messageContent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use sender peer ID hash as notification ID to group messages from same sender
|
||||||
|
val notificationId = senderPeerID.hashCode()
|
||||||
|
notificationManager.notify(notificationId, builder.build())
|
||||||
|
|
||||||
|
Log.d(TAG, "Displayed notification for $contentTitle with ID $notificationId")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSummaryNotification() {
|
||||||
|
if (pendingNotifications.isEmpty()) return
|
||||||
|
|
||||||
|
val totalMessages = pendingNotifications.values.sumOf { it.size }
|
||||||
|
val senderCount = pendingNotifications.size
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
NOTIFICATION_REQUEST_CODE,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle("bitchat")
|
||||||
|
.setContentText("$totalMessages messages from $senderCount people")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
|
.setGroup(GROUP_KEY_DM)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
|
||||||
|
// Add inbox style showing recent senders
|
||||||
|
val style = NotificationCompat.InboxStyle()
|
||||||
|
.setBigContentTitle("New Messages")
|
||||||
|
|
||||||
|
pendingNotifications.entries.take(5).forEach { (peerID, notifications) ->
|
||||||
|
val latestNotif = notifications.last()
|
||||||
|
val count = notifications.size
|
||||||
|
val line = if (count == 1) {
|
||||||
|
"${latestNotif.senderNickname}: ${latestNotif.messageContent}"
|
||||||
|
} else {
|
||||||
|
"${latestNotif.senderNickname}: $count messages"
|
||||||
|
}
|
||||||
|
style.addLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingNotifications.size > 5) {
|
||||||
|
style.setSummaryText("and ${pendingNotifications.size - 5} more conversations")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setStyle(style)
|
||||||
|
|
||||||
|
notificationManager.notify(SUMMARY_NOTIFICATION_ID, builder.build())
|
||||||
|
|
||||||
|
Log.d(TAG, "Displayed summary notification for $senderCount senders")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear notifications for a specific sender (e.g., when user opens their chat)
|
||||||
|
*/
|
||||||
|
fun clearNotificationsForSender(senderPeerID: String) {
|
||||||
|
pendingNotifications.remove(senderPeerID)
|
||||||
|
|
||||||
|
// Cancel the individual notification
|
||||||
|
val notificationId = senderPeerID.hashCode()
|
||||||
|
notificationManager.cancel(notificationId)
|
||||||
|
|
||||||
|
// Update or remove summary notification
|
||||||
|
if (pendingNotifications.isEmpty()) {
|
||||||
|
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
||||||
|
} else if (pendingNotifications.size == 1) {
|
||||||
|
// Only one sender left, remove group summary
|
||||||
|
notificationManager.cancel(SUMMARY_NOTIFICATION_ID)
|
||||||
|
} else {
|
||||||
|
// Update summary notification
|
||||||
|
showSummaryNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Cleared notifications for sender: $senderPeerID")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending notifications
|
||||||
|
*/
|
||||||
|
fun clearAllNotifications() {
|
||||||
|
pendingNotifications.clear()
|
||||||
|
notificationManager.cancelAll()
|
||||||
|
Log.d(TAG, "Cleared all notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending notification count for UI badging
|
||||||
|
*/
|
||||||
|
fun getPendingNotificationCount(): Int {
|
||||||
|
return pendingNotifications.values.sumOf { it.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending notifications for debugging
|
||||||
|
*/
|
||||||
|
fun getDebugInfo(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("Notification Manager Debug Info:")
|
||||||
|
appendLine("App in background: $isAppInBackground")
|
||||||
|
appendLine("Current private chat peer: $currentPrivateChatPeer")
|
||||||
|
appendLine("Pending notifications: ${pendingNotifications.size} senders")
|
||||||
|
pendingNotifications.forEach { (peerID, notifications) ->
|
||||||
|
appendLine(" $peerID: ${notifications.size} messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,7 @@ class PrivateChatManager(
|
|||||||
} else {
|
} else {
|
||||||
dataManager.addFavorite(fingerprint)
|
dataManager.addFavorite(fingerprint)
|
||||||
}
|
}
|
||||||
|
state.setFavoritePeers(dataManager.favoritePeers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFavorite(peerID: String): Boolean {
|
fun isFavorite(peerID: String): Boolean {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
@@ -17,6 +18,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,21 +258,21 @@ fun PeopleSection(
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Sort peers: favorites first, then by nickname
|
// Get unread private messages and private chat history for sorting
|
||||||
val sortedPeers = connectedPeers.sortedWith { peer1, peer2 ->
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
||||||
val isFav1 = viewModel.isFavorite(peer1)
|
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
||||||
val isFav2 = viewModel.isFavorite(peer2)
|
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
||||||
|
|
||||||
when {
|
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
||||||
isFav1 && !isFav2 -> -1
|
val sortedPeers = connectedPeers.sortedWith(
|
||||||
!isFav1 && isFav2 -> 1
|
compareBy<String> { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first
|
||||||
else -> {
|
.thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long)
|
||||||
val name1 = if (peer1 == nickname) "You" else (peerNicknames[peer1] ?: peer1)
|
.thenBy {
|
||||||
val name2 = if (peer2 == nickname) "You" else (peerNicknames[peer2] ?: peer2)
|
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(it)
|
||||||
name1.compareTo(name2, ignoreCase = true)
|
fingerprint == null || !favoritePeers.contains(fingerprint)
|
||||||
}
|
} // Favorites
|
||||||
}
|
.thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical
|
||||||
}
|
)
|
||||||
|
|
||||||
sortedPeers.forEach { peerID ->
|
sortedPeers.forEach { peerID ->
|
||||||
PeerItem(
|
PeerItem(
|
||||||
@@ -278,7 +280,8 @@ fun PeopleSection(
|
|||||||
displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID),
|
displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID),
|
||||||
signalStrength = peerRSSI[peerID] ?: 0,
|
signalStrength = peerRSSI[peerID] ?: 0,
|
||||||
isSelected = peerID == selectedPrivatePeer,
|
isSelected = peerID == selectedPrivatePeer,
|
||||||
isFavorite = viewModel.isFavorite(peerID),
|
isFavorite = favoritePeers.contains(viewModel.privateChatManager.getPeerFingerprint(peerID)),
|
||||||
|
hasUnreadDM = hasUnreadPrivateMessages.contains(peerID),
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
onItemClick = { onPrivateChatStart(peerID) },
|
onItemClick = { onPrivateChatStart(peerID) },
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(peerID) }
|
onToggleFavorite = { viewModel.toggleFavorite(peerID) }
|
||||||
@@ -295,6 +298,7 @@ private fun PeerItem(
|
|||||||
signalStrength: Int,
|
signalStrength: Int,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
isFavorite: Boolean,
|
isFavorite: Boolean,
|
||||||
|
hasUnreadDM: Boolean,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onToggleFavorite: () -> Unit
|
onToggleFavorite: () -> Unit
|
||||||
@@ -310,11 +314,21 @@ private fun PeerItem(
|
|||||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Signal strength indicators
|
// Show filled mail icon instead of signal strength when user has unread DMs
|
||||||
SignalStrengthIndicator(
|
if (hasUnreadDM) {
|
||||||
signalStrength = signalStrength,
|
Icon(
|
||||||
colorScheme = colorScheme
|
imageVector = Icons.Filled.Email,
|
||||||
)
|
contentDescription = "Unread messages",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Signal strength indicators
|
||||||
|
SignalStrengthIndicator(
|
||||||
|
signalStrength = signalStrength,
|
||||||
|
colorScheme = colorScheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
@@ -326,16 +340,16 @@ private fun PeerItem(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Favorite star
|
// Favorite star with proper filled/outlined states
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleFavorite,
|
onClick = onToggleFavorite,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Star,
|
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
||||||
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = if (isFavorite) colorScheme.primary else colorScheme.onSurface.copy(alpha = 0.3f)
|
tint = if (isFavorite) Color(0xFFFFD700) else colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user