mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 01:05:20 +00:00
wip: nickname disambiguation
This commit is contained in:
@@ -636,6 +636,10 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
if (connectionManager.startServices()) {
|
if (connectionManager.startServices()) {
|
||||||
isActive = true
|
isActive = true
|
||||||
|
|
||||||
|
// Add ourselves to peer manager for collision detection
|
||||||
|
val myNickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID }
|
||||||
|
peerManager.addOrUpdatePeer(myPeerID, myNickname)
|
||||||
|
|
||||||
// Start periodic announcements for peer discovery and connectivity
|
// Start periodic announcements for peer discovery and connectivity
|
||||||
sendPeriodicBroadcastAnnounce()
|
sendPeriodicBroadcastAnnounce()
|
||||||
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
|
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
|
||||||
@@ -697,6 +701,14 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return reusable
|
return reusable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update our own nickname in peer manager and announce it
|
||||||
|
*/
|
||||||
|
fun updateSelfNickname(newNickname: String) {
|
||||||
|
peerManager.addOrUpdatePeer(myPeerID, newNickname)
|
||||||
|
sendBroadcastAnnounce()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send public message
|
* Send public message
|
||||||
*/
|
*/
|
||||||
@@ -1176,6 +1188,11 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun getPeerNicknames(): Map<String, String> = peerManager.getAllPeerNicknames()
|
fun getPeerNicknames(): Map<String, String> = peerManager.getAllPeerNicknames()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get disambiguated peer nickname (nickname#suffix if collisions exist)
|
||||||
|
*/
|
||||||
|
fun getDisambiguatedNickname(peerID: String): String = peerManager.getDisambiguatedNickname(peerID)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get peer RSSI values
|
* Get peer RSSI values
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -207,22 +207,7 @@ class PeerManager {
|
|||||||
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
|
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
|
||||||
if (peerID == "unknown") return false
|
if (peerID == "unknown") return false
|
||||||
|
|
||||||
// Clean up stale peer IDs with the same nickname (exact same logic as iOS)
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val stalePeerIDs = mutableListOf<String>()
|
|
||||||
peers.forEach { (existingPeerID, info) ->
|
|
||||||
if (info.nickname == nickname && existingPeerID != peerID) {
|
|
||||||
val wasRecentlySeen = (now - info.lastSeen) < 10000
|
|
||||||
if (!wasRecentlySeen) {
|
|
||||||
stalePeerIDs.add(existingPeerID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove stale peer IDs
|
|
||||||
stalePeerIDs.forEach { stalePeerID ->
|
|
||||||
removePeer(stalePeerID, notifyDelegate = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a new peer announcement
|
// Check if this is a new peer announcement
|
||||||
val isFirstAnnounce = !announcedPeers.contains(peerID)
|
val isFirstAnnounce = !announcedPeers.contains(peerID)
|
||||||
@@ -313,6 +298,16 @@ class PeerManager {
|
|||||||
return peers[peerID]?.nickname
|
return peers[peerID]?.nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get disambiguated peer nickname (nickname#suffix if collisions exist)
|
||||||
|
*/
|
||||||
|
fun getDisambiguatedNickname(peerID: String): String {
|
||||||
|
val info = peers[peerID] ?: return peerID
|
||||||
|
val nick = info.nickname.trim()
|
||||||
|
val isAmbiguous = peers.values.count { it.nickname.trim().equals(nick, ignoreCase = true) } > 1
|
||||||
|
return if (isAmbiguous) "$nick#${peerID.takeLast(4)}" else nick
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all peer nicknames
|
* Get all peer nicknames
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,10 +37,25 @@ class GeohashRepository(
|
|||||||
fun getConversationGeohash(convKey: String): String? = conversationGeohash[convKey]
|
fun getConversationGeohash(convKey: String): String? = conversationGeohash[convKey]
|
||||||
|
|
||||||
fun findPubkeyByNickname(targetNickname: String): String? {
|
fun findPubkeyByNickname(targetNickname: String): String? {
|
||||||
return geoNicknames.entries.firstOrNull { (_, nickname) ->
|
val parts = targetNickname.split("#")
|
||||||
val base = nickname.split("#").firstOrNull() ?: nickname
|
val baseName = parts[0]
|
||||||
base == targetNickname
|
val suffix = if (parts.size > 1) parts[1] else null
|
||||||
|
|
||||||
|
if (suffix != null) {
|
||||||
|
// Exact match attempt: nickname + pubkey suffix
|
||||||
|
return geoNicknames.entries.find { (id, nick) ->
|
||||||
|
val base = nick.split("#").firstOrNull() ?: nick
|
||||||
|
base.equals(baseName, ignoreCase = true) && id.endsWith(suffix, ignoreCase = true)
|
||||||
}?.key
|
}?.key
|
||||||
|
} else {
|
||||||
|
// No suffix provided. Check for matches.
|
||||||
|
val matches = geoNicknames.entries.filter { (_, nick) ->
|
||||||
|
val base = nick.split("#").firstOrNull() ?: nick
|
||||||
|
base.equals(baseName, ignoreCase = true)
|
||||||
|
}
|
||||||
|
// If only one match, return it. If multiple, it's ambiguous (return null or handle)
|
||||||
|
return if (matches.size == 1) matches.first().key else null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// peerID alias -> nostr pubkey mapping for geohash DMs and temp aliases
|
// peerID alias -> nostr pubkey mapping for geohash DMs and temp aliases
|
||||||
|
|||||||
@@ -167,9 +167,13 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
},
|
},
|
||||||
onMessageLongPress = { message ->
|
onMessageLongPress = { message ->
|
||||||
// Message long press - open user action sheet with message context
|
// Message long press - open user action sheet with message context
|
||||||
// Extract base nickname from message sender (contains all necessary info)
|
// Use disambiguated name so actions like /block work with suffixes if collisions exist
|
||||||
val (baseName, _) = splitSuffix(message.sender)
|
val disambiguated = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
||||||
selectedUserForSheet = baseName
|
viewModel.meshService.getDisambiguatedNickname(message.senderPeerID)
|
||||||
|
} else {
|
||||||
|
message.sender
|
||||||
|
}
|
||||||
|
selectedUserForSheet = disambiguated
|
||||||
selectedMessageForSheet = message
|
selectedMessageForSheet = message
|
||||||
showUserSheet = true
|
showUserSheet = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,8 +53,21 @@ fun formatMessageAsAnnotatedString(
|
|||||||
val isSelf = message.senderPeerID == meshService.myPeerID ||
|
val isSelf = message.senderPeerID == meshService.myPeerID ||
|
||||||
message.sender == currentUserNickname ||
|
message.sender == currentUserNickname ||
|
||||||
message.sender.startsWith("$currentUserNickname#")
|
message.sender.startsWith("$currentUserNickname#")
|
||||||
|
|
||||||
if (message.sender != "system") {
|
if (message.sender != "system") {
|
||||||
|
// Resolve disambiguated sender name if it's a mesh message
|
||||||
|
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
||||||
|
val disambiguated = meshService.getDisambiguatedNickname(message.senderPeerID)
|
||||||
|
// If it returned the raw peerID because the peer is offline, fall back to message.sender (nickname at time of send)
|
||||||
|
if (disambiguated == message.senderPeerID && message.sender != message.senderPeerID) {
|
||||||
|
message.sender
|
||||||
|
} else {
|
||||||
|
disambiguated
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.sender
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Get base color for this peer (iOS-style color assignment)
|
// Get base color for this peer (iOS-style color assignment)
|
||||||
val baseColor = if (isSelf) {
|
val baseColor = if (isSelf) {
|
||||||
Color(0xFFFF9500) // Orange for self (iOS orange)
|
Color(0xFFFF9500) // Orange for self (iOS orange)
|
||||||
@@ -63,7 +76,7 @@ fun formatMessageAsAnnotatedString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Split sender into base name and hashtag suffix
|
// Split sender into base name and hashtag suffix
|
||||||
val (baseName, suffix) = splitSuffix(message.sender)
|
val (baseName, suffix) = splitSuffix(senderDisplayName)
|
||||||
|
|
||||||
// Sender prefix "<@"
|
// Sender prefix "<@"
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
@@ -85,11 +98,11 @@ fun formatMessageAsAnnotatedString(
|
|||||||
builder.append(truncatedBase)
|
builder.append(truncatedBase)
|
||||||
val nicknameEnd = builder.length
|
val nicknameEnd = builder.length
|
||||||
|
|
||||||
// Add click annotation for nickname (store canonical sender name with hash if available)
|
// Add click annotation for nickname (store disambiguated name for mentions)
|
||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
builder.addStringAnnotation(
|
builder.addStringAnnotation(
|
||||||
tag = "nickname_click",
|
tag = "nickname_click",
|
||||||
annotation = (message.originalSender ?: message.sender),
|
annotation = senderDisplayName,
|
||||||
start = nicknameStart,
|
start = nicknameStart,
|
||||||
end = nicknameEnd
|
end = nicknameEnd
|
||||||
)
|
)
|
||||||
@@ -174,8 +187,21 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
message.sender.startsWith("$currentUserNickname#")
|
message.sender.startsWith("$currentUserNickname#")
|
||||||
|
|
||||||
if (message.sender != "system") {
|
if (message.sender != "system") {
|
||||||
|
// Resolve disambiguated sender name if it's a mesh message
|
||||||
|
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
||||||
|
val disambiguated = meshService.getDisambiguatedNickname(message.senderPeerID)
|
||||||
|
// If it returned the raw peerID because the peer is offline, fall back to message.sender (nickname at time of send)
|
||||||
|
if (disambiguated == message.senderPeerID && message.sender != message.senderPeerID) {
|
||||||
|
message.sender
|
||||||
|
} else {
|
||||||
|
disambiguated
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.sender
|
||||||
|
}
|
||||||
|
|
||||||
val baseColor = if (isSelf) Color(0xFFFF9500) else getPeerColor(message, isDark)
|
val baseColor = if (isSelf) Color(0xFFFF9500) else getPeerColor(message, isDark)
|
||||||
val (baseName, suffix) = splitSuffix(message.sender)
|
val (baseName, suffix) = splitSuffix(senderDisplayName)
|
||||||
|
|
||||||
// "<@"
|
// "<@"
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
@@ -198,7 +224,7 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
if (!isSelf) {
|
if (!isSelf) {
|
||||||
builder.addStringAnnotation(
|
builder.addStringAnnotation(
|
||||||
tag = "nickname_click",
|
tag = "nickname_click",
|
||||||
annotation = (message.originalSender ?: message.sender),
|
annotation = senderDisplayName,
|
||||||
start = nicknameStart,
|
start = nicknameStart,
|
||||||
end = nicknameEnd
|
end = nicknameEnd
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ class ChatViewModel(
|
|||||||
fun setNickname(newNickname: String) {
|
fun setNickname(newNickname: String) {
|
||||||
state.setNickname(newNickname)
|
state.setNickname(newNickname)
|
||||||
dataManager.saveNickname(newNickname)
|
dataManager.saveNickname(newNickname)
|
||||||
meshService.sendBroadcastAnnounce()
|
meshService.updateSelfNickname(newNickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -519,7 +519,7 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send private message
|
// Send private message
|
||||||
val recipientNickname = meshService.getPeerNicknames()[selectedPeer]
|
val recipientNickname = meshService.getDisambiguatedNickname(selectedPeer)
|
||||||
privateChatManager.sendPrivateMessage(
|
privateChatManager.sendPrivateMessage(
|
||||||
content,
|
content,
|
||||||
selectedPeer,
|
selectedPeer,
|
||||||
@@ -581,10 +581,6 @@ class ChatViewModel(
|
|||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
fun getPeerIDForNickname(nickname: String): String? {
|
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleFavorite(peerID: String) {
|
fun toggleFavorite(peerID: String) {
|
||||||
Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID")
|
Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID")
|
||||||
privateChatManager.toggleFavorite(peerID)
|
privateChatManager.toggleFavorite(peerID)
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ class CommandProcessor(
|
|||||||
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val resolution = resolvePeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
if (peerID != null) {
|
when (resolution) {
|
||||||
|
is PeerResolutionResult.Found -> {
|
||||||
|
val peerID = resolution.peerID
|
||||||
val success = privateChatManager.startPrivateChat(peerID, meshService)
|
val success = privateChatManager.startPrivateChat(peerID, meshService)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -109,7 +111,17 @@ class CommandProcessor(
|
|||||||
messageManager.addMessage(systemMessage)
|
messageManager.addMessage(systemMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
is PeerResolutionResult.Ambiguous -> {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "multiple users found with nickname '$targetName'. please use one of: ${resolution.candidates.joinToString(", ")}",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
is PeerResolutionResult.NotFound -> {
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
content = "user '$targetName' not found. they may be offline or using a different nickname.",
|
content = "user '$targetName' not found. they may be offline or using a different nickname.",
|
||||||
@@ -118,6 +130,7 @@ class CommandProcessor(
|
|||||||
)
|
)
|
||||||
messageManager.addMessage(systemMessage)
|
messageManager.addMessage(systemMessage)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
@@ -251,7 +264,31 @@ class CommandProcessor(
|
|||||||
private fun handleBlockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleBlockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.blockPeerByNickname(targetName, meshService)
|
val resolution = resolvePeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
|
when (resolution) {
|
||||||
|
is PeerResolutionResult.Found -> {
|
||||||
|
privateChatManager.blockPeer(resolution.peerID, meshService)
|
||||||
|
}
|
||||||
|
is PeerResolutionResult.Ambiguous -> {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "multiple users found with nickname '$targetName'. please use one of: ${resolution.candidates.joinToString(", ")}",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
is PeerResolutionResult.NotFound -> {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "user '$targetName' not found.",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// List blocked users
|
// List blocked users
|
||||||
val blockedInfo = privateChatManager.listBlockedUsers()
|
val blockedInfo = privateChatManager.listBlockedUsers()
|
||||||
@@ -268,7 +305,31 @@ class CommandProcessor(
|
|||||||
private fun handleUnblockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleUnblockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.unblockPeerByNickname(targetName, meshService)
|
val resolution = resolvePeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
|
when (resolution) {
|
||||||
|
is PeerResolutionResult.Found -> {
|
||||||
|
privateChatManager.unblockPeer(resolution.peerID, meshService)
|
||||||
|
}
|
||||||
|
is PeerResolutionResult.Ambiguous -> {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "multiple users found with nickname '$targetName'. please use one of: ${resolution.candidates.joinToString(", ")}",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
is PeerResolutionResult.NotFound -> {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "user '$targetName' not found.",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
@@ -448,7 +509,12 @@ class CommandProcessor(
|
|||||||
is com.bitchat.android.geohash.ChannelID.Mesh,
|
is com.bitchat.android.geohash.ChannelID.Mesh,
|
||||||
null -> {
|
null -> {
|
||||||
// Mesh channel: use Bluetooth mesh peer nicknames
|
// Mesh channel: use Bluetooth mesh peer nicknames
|
||||||
meshService.getPeerNicknames().values.filter { it != meshService.getPeerNicknames()[meshService.myPeerID] }
|
val nicknames = meshService.getPeerNicknames().toMap() // Take a stable snapshot
|
||||||
|
val myPeerID = meshService.myPeerID
|
||||||
|
nicknames.filter { it.key != myPeerID }.map { (id, nick) ->
|
||||||
|
val isAmbiguous = nicknames.values.count { it == nick } > 1
|
||||||
|
if (isAmbiguous) "$nick#${id.takeLast(4)}" else nick
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||||
@@ -503,12 +569,39 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
private sealed class PeerResolutionResult {
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
data class Found(val peerID: String) : PeerResolutionResult()
|
||||||
|
object NotFound : PeerResolutionResult()
|
||||||
|
data class Ambiguous(val candidates: List<String>) : PeerResolutionResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePeerIDForNickname(targetName: String, meshService: BluetoothMeshService): PeerResolutionResult {
|
||||||
|
val nicknames = meshService.getPeerNicknames()
|
||||||
|
|
||||||
|
val parts = targetName.split("#")
|
||||||
|
val baseName = parts[0]
|
||||||
|
val suffix = if (parts.size > 1) parts[1] else null
|
||||||
|
|
||||||
|
if (suffix != null) {
|
||||||
|
val peerID = nicknames.entries.find { (id, nick) ->
|
||||||
|
nick.equals(baseName, ignoreCase = true) && id.takeLast(4).equals(suffix, ignoreCase = true)
|
||||||
|
}?.key
|
||||||
|
return if (peerID != null) PeerResolutionResult.Found(peerID) else PeerResolutionResult.NotFound
|
||||||
|
} else {
|
||||||
|
val matches = nicknames.entries.filter { it.value.equals(baseName, ignoreCase = true) }
|
||||||
|
return when {
|
||||||
|
matches.isEmpty() -> PeerResolutionResult.NotFound
|
||||||
|
matches.size == 1 -> PeerResolutionResult.Found(matches.first().key)
|
||||||
|
else -> {
|
||||||
|
val candidates = matches.map { "${it.value}#${it.key.takeLast(4)}" }
|
||||||
|
PeerResolutionResult.Ambiguous(candidates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
||||||
return meshService.getPeerNicknames()[peerID] ?: peerID
|
return meshService.getDisambiguatedNickname(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class MeshDelegateHandler(
|
|||||||
|
|
||||||
// Show notification with enhanced information - now includes senderPeerID
|
// Show notification with enhanced information - now includes senderPeerID
|
||||||
message.senderPeerID?.let { senderPeerID ->
|
message.senderPeerID?.let { senderPeerID ->
|
||||||
// Use nickname if available, fall back to sender or senderPeerID
|
// Use disambiguated nickname for notifications
|
||||||
val senderNickname = message.sender.takeIf { it != senderPeerID } ?: senderPeerID
|
val senderNickname = getMeshService().getDisambiguatedNickname(senderPeerID)
|
||||||
val preview = NotificationTextUtils.buildPrivateMessagePreview(message)
|
val preview = NotificationTextUtils.buildPrivateMessagePreview(message)
|
||||||
notificationManager.showPrivateMessageNotification(
|
notificationManager.showPrivateMessageNotification(
|
||||||
senderPeerID = senderPeerID,
|
senderPeerID = senderPeerID,
|
||||||
@@ -250,10 +250,11 @@ class MeshDelegateHandler(
|
|||||||
val isMention = checkForMeshMention(message.content, currentNickname)
|
val isMention = checkForMeshMention(message.content, currentNickname)
|
||||||
|
|
||||||
if (isMention) {
|
if (isMention) {
|
||||||
android.util.Log.d("MeshDelegateHandler", "🔔 Triggering mesh mention notification from ${message.sender}")
|
val senderNickname = message.senderPeerID?.let { getMeshService().getDisambiguatedNickname(it) } ?: message.sender
|
||||||
|
android.util.Log.d("MeshDelegateHandler", "🔔 Triggering mesh mention notification from $senderNickname")
|
||||||
|
|
||||||
notificationManager.showMeshMentionNotification(
|
notificationManager.showMeshMentionNotification(
|
||||||
senderNickname = message.sender,
|
senderNickname = senderNickname,
|
||||||
messageContent = message.content,
|
messageContent = message.content,
|
||||||
senderPeerID = message.senderPeerID
|
senderPeerID = message.senderPeerID
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.bitchat.android.geohash.ChannelID
|
|||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||||
import com.bitchat.android.nostr.GeohashConversationRegistry
|
import com.bitchat.android.nostr.GeohashConversationRegistry
|
||||||
|
import com.bitchat.android.util.toHexString
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,7 +368,7 @@ fun PeopleSection(
|
|||||||
// Offline favorites (exclude ones mapped to connected)
|
// Offline favorites (exclude ones mapped to connected)
|
||||||
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
||||||
offlineFavorites.forEach { fav ->
|
offlineFavorites.forEach { fav ->
|
||||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
val favPeerID = fav.peerNoisePublicKey.toHexString()
|
||||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||||
if (!isMappedToConnected) {
|
if (!isMappedToConnected) {
|
||||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||||
@@ -435,7 +436,7 @@ fun PeopleSection(
|
|||||||
|
|
||||||
// Append offline favorites we actively favorite (and not currently connected)
|
// Append offline favorites we actively favorite (and not currently connected)
|
||||||
offlineFavorites.forEach { fav ->
|
offlineFavorites.forEach { fav ->
|
||||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
val favPeerID = fav.peerNoisePublicKey.toHexString()
|
||||||
// If any connected peer maps to this noise key, skip showing the offline entry
|
// If any connected peer maps to this noise key, skip showing the offline entry
|
||||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||||
if (isMappedToConnected) return@forEach
|
if (isMappedToConnected) return@forEach
|
||||||
@@ -560,7 +561,7 @@ private fun PeerItem(
|
|||||||
// Split display name for hashtag suffix support (iOS-compatible)
|
// Split display name for hashtag suffix support (iOS-compatible)
|
||||||
val (baseNameRaw, suffixRaw) = splitSuffix(displayName)
|
val (baseNameRaw, suffixRaw) = splitSuffix(displayName)
|
||||||
val baseName = truncateNickname(baseNameRaw)
|
val baseName = truncateNickname(baseNameRaw)
|
||||||
val suffix = if (showHashSuffix) suffixRaw else ""
|
val suffix = if (showHashSuffix && suffixRaw.isEmpty()) "#${peerID.takeLast(4)}" else if (showHashSuffix) suffixRaw else ""
|
||||||
val isMe = displayName == "You" || peerID == currentNickname
|
val isMe = displayName == "You" || peerID == currentNickname
|
||||||
|
|
||||||
// Get consistent peer color (iOS-compatible)
|
// Get consistent peer color (iOS-compatible)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class PrivateChatManager(
|
|||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
dataManager.addBlockedUser(fingerprint)
|
dataManager.addBlockedUser(fingerprint)
|
||||||
|
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = meshService.getDisambiguatedNickname(peerID)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
content = "blocked user $peerNickname",
|
content = "blocked user $peerNickname",
|
||||||
@@ -217,7 +217,7 @@ class PrivateChatManager(
|
|||||||
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
||||||
dataManager.removeBlockedUser(fingerprint)
|
dataManager.removeBlockedUser(fingerprint)
|
||||||
|
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = meshService.getDisambiguatedNickname(peerID)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
content = "unblocked user $peerNickname",
|
content = "unblocked user $peerNickname",
|
||||||
@@ -230,52 +230,6 @@ class PrivateChatManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
|
||||||
|
|
||||||
if (peerID != null) {
|
|
||||||
return blockPeer(peerID, meshService)
|
|
||||||
} else {
|
|
||||||
val systemMessage = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = "user '$targetName' not found",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false
|
|
||||||
)
|
|
||||||
messageManager.addMessage(systemMessage)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unblockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
|
||||||
|
|
||||||
if (peerID != null) {
|
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
|
||||||
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
|
||||||
return unblockPeer(peerID, meshService)
|
|
||||||
} else {
|
|
||||||
val systemMessage = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = "user '$targetName' is not blocked",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false
|
|
||||||
)
|
|
||||||
messageManager.addMessage(systemMessage)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val systemMessage = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = "user '$targetName' not found",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false
|
|
||||||
)
|
|
||||||
messageManager.addMessage(systemMessage)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun listBlockedUsers(): String {
|
fun listBlockedUsers(): String {
|
||||||
val blockedCount = dataManager.blockedUsers.size
|
val blockedCount = dataManager.blockedUsers.size
|
||||||
return if (blockedCount == 0) {
|
return if (blockedCount == 0) {
|
||||||
@@ -413,12 +367,8 @@ class PrivateChatManager(
|
|||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
||||||
return meshService.getPeerNicknames()[peerID] ?: peerID
|
return meshService.getDisambiguatedNickname(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Consolidation
|
// MARK: - Consolidation
|
||||||
|
|||||||
Reference in New Issue
Block a user