mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 06:25:21 +00:00
Geohash notifications (#288)
* mention notifications * notifications * warning
This commit is contained in:
@@ -658,7 +658,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle intents from notification clicks - open specific private chat
|
||||
* Handle intents from notification clicks - open specific private chat or geohash chat
|
||||
*/
|
||||
private fun handleNotificationIntent(intent: Intent) {
|
||||
val shouldOpenPrivateChat = intent.getBooleanExtra(
|
||||
@@ -666,18 +666,52 @@ class MainActivity : ComponentActivity() {
|
||||
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)
|
||||
val shouldOpenGeohashChat = intent.getBooleanExtra(
|
||||
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_GEOHASH_CHAT,
|
||||
false
|
||||
)
|
||||
|
||||
when {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (peerID != null) {
|
||||
Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
|
||||
shouldOpenGeohashChat -> {
|
||||
val geohash = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_GEOHASH)
|
||||
|
||||
// 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)
|
||||
if (geohash != null) {
|
||||
Log.d("MainActivity", "Opening geohash chat #$geohash from notification")
|
||||
|
||||
// Switch to the geohash channel - create appropriate geohash channel level
|
||||
val level = when (geohash.length) {
|
||||
7 -> com.bitchat.android.geohash.GeohashChannelLevel.BLOCK
|
||||
6 -> com.bitchat.android.geohash.GeohashChannelLevel.NEIGHBORHOOD
|
||||
5 -> com.bitchat.android.geohash.GeohashChannelLevel.CITY
|
||||
4 -> com.bitchat.android.geohash.GeohashChannelLevel.REGION
|
||||
2 -> com.bitchat.android.geohash.GeohashChannelLevel.COUNTRY
|
||||
else -> com.bitchat.android.geohash.GeohashChannelLevel.CITY // Default fallback
|
||||
}
|
||||
val geohashChannel = com.bitchat.android.geohash.GeohashChannel(level, geohash)
|
||||
val channelId = com.bitchat.android.geohash.ChannelID.Location(geohashChannel)
|
||||
chatViewModel.selectLocationChannel(channelId)
|
||||
|
||||
// Update current geohash state for notifications
|
||||
chatViewModel.setCurrentGeohash(geohash)
|
||||
|
||||
// Clear notifications for this geohash since user is now viewing it
|
||||
chatViewModel.clearNotificationsForGeohash(geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ class NostrGeohashService(
|
||||
private val privateChatManager: PrivateChatManager,
|
||||
private val meshDelegateHandler: MeshDelegateHandler,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dataManager: com.bitchat.android.ui.DataManager
|
||||
private val dataManager: com.bitchat.android.ui.DataManager,
|
||||
private val notificationManager: com.bitchat.android.ui.NotificationManager
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -51,7 +52,8 @@ class NostrGeohashService(
|
||||
privateChatManager: PrivateChatManager,
|
||||
meshDelegateHandler: MeshDelegateHandler,
|
||||
coroutineScope: CoroutineScope,
|
||||
dataManager: com.bitchat.android.ui.DataManager
|
||||
dataManager: com.bitchat.android.ui.DataManager,
|
||||
notificationManager: com.bitchat.android.ui.NotificationManager
|
||||
): NostrGeohashService {
|
||||
return synchronized(this) {
|
||||
INSTANCE ?: NostrGeohashService(
|
||||
@@ -61,7 +63,8 @@ class NostrGeohashService(
|
||||
privateChatManager,
|
||||
meshDelegateHandler,
|
||||
coroutineScope,
|
||||
dataManager
|
||||
dataManager,
|
||||
notificationManager
|
||||
).also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
@@ -751,6 +754,10 @@ class NostrGeohashService(
|
||||
Log.d(TAG, "📡 Switched to mesh channel")
|
||||
// Immediate UI state updates
|
||||
currentGeohash = null
|
||||
// Update notification manager with current geohash
|
||||
notificationManager.setCurrentGeohash(null)
|
||||
// Clear mesh mention notifications since user is now viewing mesh chat
|
||||
notificationManager.clearMeshMentionNotifications()
|
||||
// Note: Don't clear geoNicknames - keep cached for when we return to location channels
|
||||
stopGeoParticipantsTimer()
|
||||
state.setGeohashPeople(emptyList())
|
||||
@@ -760,6 +767,10 @@ class NostrGeohashService(
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
Log.d(TAG, "📍 Switching to geohash channel: ${channel.channel.geohash}")
|
||||
currentGeohash = channel.channel.geohash
|
||||
// Update notification manager with current geohash
|
||||
notificationManager.setCurrentGeohash(channel.channel.geohash)
|
||||
// Clear notifications for this geohash since user is now viewing it
|
||||
notificationManager.clearNotificationsForGeohash(channel.channel.geohash)
|
||||
// Note: Don't clear geoNicknames - they contain cached nicknames for all geohashes
|
||||
|
||||
// Load stored messages for this geohash immediately
|
||||
@@ -1023,6 +1034,9 @@ class NostrGeohashService(
|
||||
messageManager.addMessage(message)
|
||||
}
|
||||
|
||||
// NOTIFICATION LOGIC: Check for mentions and first messages
|
||||
checkAndTriggerGeohashNotifications(geohash, senderName, content, message)
|
||||
|
||||
Log.d(TAG, "📥 Unified geohash event processed - geohash: $geohash, sender: $senderName, content: ${content.take(50)}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -1030,6 +1044,104 @@ class NostrGeohashService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and trigger geohash notifications for mentions and first messages
|
||||
*/
|
||||
private fun checkAndTriggerGeohashNotifications(
|
||||
geohash: String,
|
||||
senderName: String,
|
||||
content: String,
|
||||
message: BitchatMessage
|
||||
) {
|
||||
try {
|
||||
// Get user's current nickname
|
||||
val currentNickname = state.getNicknameValue()
|
||||
if (currentNickname.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this message mentions the current user
|
||||
val isMention = checkForMention(content, currentNickname)
|
||||
|
||||
// Check if this is the first message in a subscribed geohash chat
|
||||
val isFirstMessage = checkIfFirstMessage(geohash, message)
|
||||
|
||||
// Only trigger notifications if we have a mention or first message
|
||||
if (isMention || isFirstMessage) {
|
||||
Log.d(TAG, "🔔 Triggering geohash notification - geohash: $geohash, mention: $isMention, first: $isFirstMessage")
|
||||
|
||||
notificationManager.showGeohashNotification(
|
||||
geohash = geohash,
|
||||
senderNickname = senderName,
|
||||
messageContent = content,
|
||||
isMention = isMention,
|
||||
isFirstMessage = isFirstMessage
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking geohash notifications: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the content mentions the current user with @nickname#hash format
|
||||
*/
|
||||
private fun checkForMention(content: String, currentNickname: String): Boolean {
|
||||
// iOS-style mention pattern: @nickname#1234 or @nickname
|
||||
val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex()
|
||||
|
||||
return mentionPattern.findAll(content).any { match ->
|
||||
val mentionWithoutAt = match.groupValues[1]
|
||||
// Split the mention to get base nickname (without #hash suffix)
|
||||
val baseName = if (mentionWithoutAt.contains("#")) {
|
||||
mentionWithoutAt.substringBeforeLast("#")
|
||||
} else {
|
||||
mentionWithoutAt
|
||||
}
|
||||
|
||||
// Check if the base name matches current user's nickname
|
||||
baseName.equals(currentNickname, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is the first message in a subscribed geohash chat
|
||||
*/
|
||||
private fun checkIfFirstMessage(geohash: String, message: BitchatMessage): Boolean {
|
||||
// Get the message history for this geohash
|
||||
val messageHistory = geohashMessageHistory[geohash] ?: return true
|
||||
|
||||
// Filter out our own messages (local echoes and messages from our identity)
|
||||
val otherUserMessages = messageHistory.filter { msg ->
|
||||
// Check if this is our own message by comparing sender ID with our geohash identity
|
||||
try {
|
||||
val myGeoIdentity = NostrIdentityBridge.deriveIdentity(
|
||||
forGeohash = geohash,
|
||||
context = application
|
||||
)
|
||||
val myIdentityId = "nostr:${myGeoIdentity.publicKeyHex.take(8)}"
|
||||
msg.senderPeerID != myIdentityId && msg.senderPeerID != message.senderPeerID
|
||||
} catch (e: Exception) {
|
||||
// If we can't determine identity, assume it's not our message
|
||||
msg.senderPeerID != message.senderPeerID
|
||||
}
|
||||
}
|
||||
|
||||
// This is a first message if there are no other user messages and this isn't from us
|
||||
val isFromUs = try {
|
||||
val myGeoIdentity = NostrIdentityBridge.deriveIdentity(
|
||||
forGeohash = geohash,
|
||||
context = application
|
||||
)
|
||||
val myIdentityId = "nostr:${myGeoIdentity.publicKeyHex.take(8)}"
|
||||
message.senderPeerID == myIdentityId
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
return otherUserMessages.isEmpty() && !isFromUs
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle geohash DM event (private messages in geohash context) - iOS compatible
|
||||
*/
|
||||
|
||||
@@ -69,7 +69,8 @@ class ChatViewModel(
|
||||
privateChatManager = privateChatManager,
|
||||
meshDelegateHandler = meshDelegateHandler,
|
||||
coroutineScope = viewModelScope,
|
||||
dataManager = dataManager
|
||||
dataManager = dataManager,
|
||||
notificationManager = notificationManager
|
||||
)
|
||||
|
||||
// Expose state through LiveData (maintaining the same interface)
|
||||
@@ -213,6 +214,8 @@ class ChatViewModel(
|
||||
privateChatManager.endPrivateChat()
|
||||
// Notify notification manager that no private chat is active
|
||||
setCurrentPrivateChatPeer(null)
|
||||
// Clear mesh mention notifications since user is now back in mesh chat
|
||||
clearMeshMentionNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Message Sending
|
||||
@@ -388,11 +391,28 @@ class ChatViewModel(
|
||||
notificationManager.setCurrentPrivateChatPeer(peerID)
|
||||
}
|
||||
|
||||
fun setCurrentGeohash(geohash: String?) {
|
||||
// Update notification manager with current geohash for notification logic
|
||||
notificationManager.setCurrentGeohash(geohash)
|
||||
}
|
||||
|
||||
fun clearNotificationsForSender(peerID: String) {
|
||||
// Clear notifications when user opens a chat
|
||||
notificationManager.clearNotificationsForSender(peerID)
|
||||
}
|
||||
|
||||
fun clearNotificationsForGeohash(geohash: String) {
|
||||
// Clear notifications when user opens a geohash chat
|
||||
notificationManager.clearNotificationsForGeohash(geohash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear mesh mention notifications when user opens mesh chat
|
||||
*/
|
||||
fun clearMeshMentionNotifications() {
|
||||
notificationManager.clearMeshMentionNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Command Autocomplete (delegated)
|
||||
|
||||
fun updateCommandSuggestions(input: String) {
|
||||
|
||||
@@ -102,7 +102,7 @@ fun LocationChannelsSheet(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps.",
|
||||
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.",
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
|
||||
@@ -70,6 +70,9 @@ class MeshDelegateHandler(
|
||||
} else {
|
||||
// Public message
|
||||
messageManager.addMessage(message)
|
||||
|
||||
// Check for mentions in mesh chat
|
||||
checkAndTriggerMeshMentionNotification(message)
|
||||
}
|
||||
|
||||
// Periodic cleanup
|
||||
@@ -124,6 +127,48 @@ class MeshDelegateHandler(
|
||||
return privateChatManager.isFavorite(peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for mentions in mesh messages and trigger notifications
|
||||
*/
|
||||
private fun checkAndTriggerMeshMentionNotification(message: BitchatMessage) {
|
||||
try {
|
||||
// Get user's current nickname
|
||||
val currentNickname = state.getNicknameValue()
|
||||
if (currentNickname.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this message mentions the current user using @username format
|
||||
val isMention = checkForMeshMention(message.content, currentNickname)
|
||||
|
||||
if (isMention) {
|
||||
android.util.Log.d("MeshDelegateHandler", "🔔 Triggering mesh mention notification from ${message.sender}")
|
||||
|
||||
notificationManager.showMeshMentionNotification(
|
||||
senderNickname = message.sender,
|
||||
messageContent = message.content,
|
||||
senderPeerID = message.senderPeerID
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MeshDelegateHandler", "Error checking mesh mentions: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the content mentions the current user with @username format (simple, no hash suffix)
|
||||
*/
|
||||
private fun checkForMeshMention(content: String, currentNickname: String): Boolean {
|
||||
// Simple mention pattern for mesh: @username (no hash suffix like geohash)
|
||||
val mentionPattern = "@([\\p{L}0-9_]+)".toRegex()
|
||||
|
||||
return mentionPattern.findAll(content).any { match ->
|
||||
val mentionedUsername = match.groupValues[1]
|
||||
// Direct comparison for mesh mentions (no hash suffix to remove)
|
||||
mentionedUsername.equals(currentNickname, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send read receipts reactively based on UI focus state.
|
||||
* Uses same logic as notification system - send read receipt if user is currently
|
||||
|
||||
@@ -15,10 +15,12 @@ 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
|
||||
* Enhanced notification manager for direct messages and geohash chats with production-ready features:
|
||||
* - Notification grouping per sender/geohash
|
||||
* - Click handling to open specific DM or geohash chat
|
||||
* - App background state awareness
|
||||
* - Support for mention notifications in geohash chats
|
||||
* - Support for first message notifications in geohash chats
|
||||
* - Proper notification management and cleanup
|
||||
*/
|
||||
class NotificationManager(private val context: Context) {
|
||||
@@ -26,14 +28,20 @@ class NotificationManager(private val context: Context) {
|
||||
companion object {
|
||||
private const val TAG = "NotificationManager"
|
||||
private const val CHANNEL_ID = "bitchat_dm_notifications"
|
||||
private const val GEOHASH_CHANNEL_ID = "bitchat_geohash_notifications"
|
||||
private const val GROUP_KEY_DM = "bitchat_dm_group"
|
||||
private const val GROUP_KEY_GEOHASH = "bitchat_geohash_group"
|
||||
private const val NOTIFICATION_REQUEST_CODE = 1000
|
||||
private const val GEOHASH_NOTIFICATION_REQUEST_CODE = 2000
|
||||
private const val SUMMARY_NOTIFICATION_ID = 999
|
||||
private const val GEOHASH_SUMMARY_NOTIFICATION_ID = 998
|
||||
|
||||
// Intent extras for notification handling
|
||||
const val EXTRA_OPEN_PRIVATE_CHAT = "open_private_chat"
|
||||
const val EXTRA_OPEN_GEOHASH_CHAT = "open_geohash_chat"
|
||||
const val EXTRA_PEER_ID = "peer_id"
|
||||
const val EXTRA_SENDER_NICKNAME = "sender_nickname"
|
||||
const val EXTRA_GEOHASH = "geohash"
|
||||
}
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
@@ -41,6 +49,7 @@ class NotificationManager(private val context: Context) {
|
||||
|
||||
// Track pending notifications per sender to enable grouping
|
||||
private val pendingNotifications = ConcurrentHashMap<String, MutableList<PendingNotification>>()
|
||||
private val pendingGeohashNotifications = ConcurrentHashMap<String, MutableList<GeohashNotification>>()
|
||||
|
||||
// Track app background state
|
||||
@Volatile
|
||||
@@ -49,6 +58,9 @@ class NotificationManager(private val context: Context) {
|
||||
// Track current view state
|
||||
@Volatile
|
||||
private var currentPrivateChatPeer: String? = null
|
||||
|
||||
@Volatile
|
||||
private var currentGeohash: String? = null
|
||||
|
||||
data class PendingNotification(
|
||||
val senderPeerID: String,
|
||||
@@ -56,6 +68,15 @@ class NotificationManager(private val context: Context) {
|
||||
val messageContent: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
data class GeohashNotification(
|
||||
val geohash: String,
|
||||
val senderNickname: String,
|
||||
val messageContent: String,
|
||||
val timestamp: Long,
|
||||
val isMention: Boolean,
|
||||
val isFirstMessage: Boolean
|
||||
)
|
||||
|
||||
init {
|
||||
createNotificationChannel()
|
||||
@@ -63,15 +84,27 @@ class NotificationManager(private val context: Context) {
|
||||
|
||||
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
|
||||
// DM notifications channel
|
||||
val dmName = "Direct Messages"
|
||||
val dmDescriptionText = "Notifications for private messages from other users"
|
||||
val dmImportance = NotificationManager.IMPORTANCE_HIGH
|
||||
val dmChannel = NotificationChannel(CHANNEL_ID, dmName, dmImportance).apply {
|
||||
description = dmDescriptionText
|
||||
enableVibration(true)
|
||||
setShowBadge(true)
|
||||
}
|
||||
systemNotificationManager.createNotificationChannel(channel)
|
||||
systemNotificationManager.createNotificationChannel(dmChannel)
|
||||
|
||||
// Geohash notifications channel
|
||||
val geohashName = "Geohash Chats"
|
||||
val geohashDescriptionText = "Notifications for mentions and messages in geohash location channels"
|
||||
val geohashImportance = NotificationManager.IMPORTANCE_HIGH
|
||||
val geohashChannel = NotificationChannel(GEOHASH_CHANNEL_ID, geohashName, geohashImportance).apply {
|
||||
description = geohashDescriptionText
|
||||
enableVibration(true)
|
||||
setShowBadge(true)
|
||||
}
|
||||
systemNotificationManager.createNotificationChannel(geohashChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +123,14 @@ class NotificationManager(private val context: Context) {
|
||||
currentPrivateChatPeer = peerID
|
||||
Log.d(TAG, "Current private chat peer changed: $peerID")
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current geohash - affects notification logic for geohash chats
|
||||
*/
|
||||
fun setCurrentGeohash(geohash: String?) {
|
||||
currentGeohash = geohash
|
||||
Log.d(TAG, "Current geohash changed: $geohash")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification for a private message with proper grouping and state awareness
|
||||
@@ -290,11 +331,370 @@ class NotificationManager(private val context: Context) {
|
||||
Log.d(TAG, "Cleared notifications for sender: $senderPeerID")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification for a geohash message with mention or first message
|
||||
*/
|
||||
fun showGeohashNotification(
|
||||
geohash: String,
|
||||
senderNickname: String,
|
||||
messageContent: String,
|
||||
isMention: Boolean = false,
|
||||
isFirstMessage: Boolean = false
|
||||
) {
|
||||
// Only show notifications if app is in background OR user is not viewing this specific geohash
|
||||
val shouldNotify = isAppInBackground || (!isAppInBackground && currentGeohash != geohash)
|
||||
|
||||
if (!shouldNotify) {
|
||||
Log.d(TAG, "Skipping geohash notification - app in foreground and viewing geohash $geohash")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Showing geohash notification for $geohash from $senderNickname (mention: $isMention, first: $isFirstMessage)")
|
||||
|
||||
val notification = GeohashNotification(
|
||||
geohash = geohash,
|
||||
senderNickname = senderNickname,
|
||||
messageContent = messageContent,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
isMention = isMention,
|
||||
isFirstMessage = isFirstMessage
|
||||
)
|
||||
|
||||
// Add to pending notifications for this geohash
|
||||
pendingGeohashNotifications.computeIfAbsent(geohash) { mutableListOf() }.add(notification)
|
||||
|
||||
// Create or update notification for this geohash
|
||||
showNotificationForGeohash(geohash)
|
||||
|
||||
// Update summary notification if we have multiple geohashes
|
||||
if (pendingGeohashNotifications.size > 1) {
|
||||
showGeohashSummaryNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationForGeohash(geohash: String) {
|
||||
val notifications = pendingGeohashNotifications[geohash] ?: return
|
||||
if (notifications.isEmpty()) return
|
||||
|
||||
val latestNotification = notifications.last()
|
||||
val messageCount = notifications.size
|
||||
val mentionCount = notifications.count { it.isMention }
|
||||
val firstMessageCount = notifications.count { it.isFirstMessage }
|
||||
|
||||
// Create intent to open the specific geohash chat
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_OPEN_GEOHASH_CHAT, true)
|
||||
putExtra(EXTRA_GEOHASH, geohash)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
GEOHASH_NOTIFICATION_REQUEST_CODE + geohash.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// Build notification content
|
||||
val contentTitle = when {
|
||||
mentionCount > 0 && firstMessageCount > 0 -> "Mentioned in #$geohash (+${messageCount - 1} more)"
|
||||
mentionCount > 0 -> if (mentionCount == 1) "Mentioned in #$geohash" else "$mentionCount mentions in #$geohash"
|
||||
firstMessageCount > 0 -> "New activity in #$geohash"
|
||||
else -> "Messages in #$geohash"
|
||||
}
|
||||
|
||||
val contentText = when {
|
||||
latestNotification.isMention -> "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
|
||||
latestNotification.isFirstMessage -> "${latestNotification.senderNickname} joined the conversation"
|
||||
else -> "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, GEOHASH_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(contentTitle)
|
||||
.setContentText(contentText)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(if (latestNotification.isMention) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setShowWhen(true)
|
||||
.setWhen(latestNotification.timestamp)
|
||||
|
||||
// Add to notification group if we have multiple geohashes
|
||||
if (pendingGeohashNotifications.size > 1) {
|
||||
builder.setGroup(GROUP_KEY_GEOHASH)
|
||||
}
|
||||
|
||||
// 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 ->
|
||||
val prefix = when {
|
||||
notif.isMention -> "💬 "
|
||||
notif.isFirstMessage -> "👋 "
|
||||
else -> ""
|
||||
}
|
||||
style.addLine("$prefix${notif.senderNickname}: ${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(contentText)
|
||||
)
|
||||
}
|
||||
|
||||
// Use geohash hash as notification ID to group messages from same geohash
|
||||
val notificationId = 3000 + geohash.hashCode()
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
|
||||
Log.d(TAG, "Displayed geohash notification for $contentTitle with ID $notificationId")
|
||||
}
|
||||
|
||||
private fun showGeohashSummaryNotification() {
|
||||
if (pendingGeohashNotifications.isEmpty()) return
|
||||
|
||||
val totalMessages = pendingGeohashNotifications.values.sumOf { it.size }
|
||||
val geohashCount = pendingGeohashNotifications.size
|
||||
val totalMentions = pendingGeohashNotifications.values.sumOf { notifications ->
|
||||
notifications.count { it.isMention }
|
||||
}
|
||||
|
||||
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,
|
||||
GEOHASH_NOTIFICATION_REQUEST_CODE,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val contentTitle = if (totalMentions > 0) {
|
||||
"bitchat - $totalMentions mentions"
|
||||
} else {
|
||||
"bitchat - location chats"
|
||||
}
|
||||
|
||||
val contentText = "$totalMessages messages from $geohashCount locations"
|
||||
|
||||
val builder = NotificationCompat.Builder(context, GEOHASH_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(contentTitle)
|
||||
.setContentText(contentText)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setGroup(GROUP_KEY_GEOHASH)
|
||||
.setGroupSummary(true)
|
||||
|
||||
// Add inbox style showing recent geohashes
|
||||
val style = NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle("New Location Messages")
|
||||
|
||||
pendingGeohashNotifications.entries.take(5).forEach { (geohash, notifications) ->
|
||||
val mentionCount = notifications.count { it.isMention }
|
||||
val messageCount = notifications.size
|
||||
val line = when {
|
||||
mentionCount > 0 -> "#$geohash: $mentionCount mentions (+${messageCount - mentionCount} more)"
|
||||
messageCount == 1 -> "#$geohash: 1 message"
|
||||
else -> "#$geohash: $messageCount messages"
|
||||
}
|
||||
style.addLine(line)
|
||||
}
|
||||
|
||||
if (pendingGeohashNotifications.size > 5) {
|
||||
style.setSummaryText("and ${pendingGeohashNotifications.size - 5} more locations")
|
||||
}
|
||||
|
||||
builder.setStyle(style)
|
||||
|
||||
notificationManager.notify(GEOHASH_SUMMARY_NOTIFICATION_ID, builder.build())
|
||||
|
||||
Log.d(TAG, "Displayed geohash summary notification for $geohashCount locations")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear notifications for a specific geohash (e.g., when user opens that chat)
|
||||
*/
|
||||
fun clearNotificationsForGeohash(geohash: String) {
|
||||
pendingGeohashNotifications.remove(geohash)
|
||||
|
||||
// Cancel the individual notification
|
||||
val notificationId = 3000 + geohash.hashCode()
|
||||
notificationManager.cancel(notificationId)
|
||||
|
||||
// Update or remove summary notification
|
||||
if (pendingGeohashNotifications.isEmpty()) {
|
||||
notificationManager.cancel(GEOHASH_SUMMARY_NOTIFICATION_ID)
|
||||
} else if (pendingGeohashNotifications.size == 1) {
|
||||
// Only one geohash left, remove group summary
|
||||
notificationManager.cancel(GEOHASH_SUMMARY_NOTIFICATION_ID)
|
||||
} else {
|
||||
// Update summary notification
|
||||
showGeohashSummaryNotification()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Cleared notifications for geohash: $geohash")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification for a mesh mention (@username format)
|
||||
*/
|
||||
fun showMeshMentionNotification(
|
||||
senderNickname: String,
|
||||
messageContent: String,
|
||||
senderPeerID: String? = null
|
||||
) {
|
||||
// Only show notifications if app is in background OR user is not viewing mesh chat
|
||||
// User is viewing mesh chat when: not in private chat AND not in geohash chat
|
||||
val isViewingMeshChat = currentPrivateChatPeer == null && currentGeohash == null
|
||||
val shouldNotify = isAppInBackground || (!isAppInBackground && !isViewingMeshChat)
|
||||
|
||||
if (!shouldNotify) {
|
||||
Log.d(TAG, "Skipping mesh mention notification - app in foreground and viewing mesh chat")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Showing mesh mention notification from $senderNickname")
|
||||
|
||||
// Use a special key for mesh mentions to group them together
|
||||
val meshMentionKey = "mesh_mentions"
|
||||
val notification = PendingNotification(
|
||||
senderPeerID = senderPeerID ?: meshMentionKey,
|
||||
senderNickname = senderNickname,
|
||||
messageContent = messageContent,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// Add to pending notifications for mesh mentions
|
||||
pendingNotifications.computeIfAbsent(meshMentionKey) { mutableListOf() }.add(notification)
|
||||
|
||||
// Create or update notification for mesh mentions
|
||||
showNotificationForMeshMentions()
|
||||
|
||||
// Update summary notification if we have multiple senders
|
||||
if (pendingNotifications.size > 1) {
|
||||
showSummaryNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationForMeshMentions() {
|
||||
val notifications = pendingNotifications["mesh_mentions"] ?: return
|
||||
if (notifications.isEmpty()) return
|
||||
|
||||
val latestNotification = notifications.last()
|
||||
val messageCount = notifications.size
|
||||
|
||||
// Create intent to open the mesh chat (no specific peer, just main chat)
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
// No specific chat to open, just bring the app to foreground
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
NOTIFICATION_REQUEST_CODE + "mesh_mentions".hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// Build notification content
|
||||
val contentTitle = if (messageCount == 1) {
|
||||
"Mentioned in Mesh Chat"
|
||||
} else {
|
||||
"$messageCount mentions in Mesh Chat"
|
||||
}
|
||||
|
||||
val contentText = "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
|
||||
|
||||
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)
|
||||
.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.senderNickname}: ${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(contentText)
|
||||
)
|
||||
}
|
||||
|
||||
// Use a special notification ID for mesh mentions
|
||||
val notificationId = 4000 // Different from DM and geohash IDs
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
|
||||
Log.d(TAG, "Displayed mesh mention notification: $contentTitle")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear mesh mention notifications (when user opens mesh chat)
|
||||
*/
|
||||
fun clearMeshMentionNotifications() {
|
||||
pendingNotifications.remove("mesh_mentions")
|
||||
|
||||
// Cancel the mesh mention notification
|
||||
val notificationId = 4000
|
||||
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 mesh mention notifications")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending notifications
|
||||
*/
|
||||
fun clearAllNotifications() {
|
||||
pendingNotifications.clear()
|
||||
pendingGeohashNotifications.clear()
|
||||
notificationManager.cancelAll()
|
||||
Log.d(TAG, "Cleared all notifications")
|
||||
}
|
||||
@@ -303,7 +703,8 @@ class NotificationManager(private val context: Context) {
|
||||
* Get pending notification count for UI badging
|
||||
*/
|
||||
fun getPendingNotificationCount(): Int {
|
||||
return pendingNotifications.values.sumOf { it.size }
|
||||
return pendingNotifications.values.sumOf { it.size } +
|
||||
pendingGeohashNotifications.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,10 +729,17 @@ class NotificationManager(private val context: Context) {
|
||||
appendLine("Notification Manager Debug Info:")
|
||||
appendLine("App in background: $isAppInBackground")
|
||||
appendLine("Current private chat peer: $currentPrivateChatPeer")
|
||||
appendLine("Pending notifications: ${pendingNotifications.size} senders")
|
||||
appendLine("Current geohash: $currentGeohash")
|
||||
appendLine("Pending DM notifications: ${pendingNotifications.size} senders")
|
||||
pendingNotifications.forEach { (peerID, notifications) ->
|
||||
appendLine(" $peerID: ${notifications.size} messages")
|
||||
}
|
||||
appendLine("Pending geohash notifications: ${pendingGeohashNotifications.size} geohashes")
|
||||
pendingGeohashNotifications.forEach { (geohash, notifications) ->
|
||||
val mentions = notifications.count { it.isMention }
|
||||
val firstMessages = notifications.count { it.isFirstMessage }
|
||||
appendLine(" #$geohash: ${notifications.size} messages ($mentions mentions, $firstMessages first messages)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user