From 9c103180cdae88a54268030866fceb11b3369c33 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:27:55 +0200 Subject: [PATCH] limit nick length (#330) --- .../com/bitchat/android/ui/ChatUIUtils.kt | 7 +- .../bitchat/android/ui/GeohashPeopleList.kt | 23 ++++- .../bitchat/android/ui/SidebarComponents.kt | 89 ++++++++++++++++--- 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt index d1e34307..4e98a4e0 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -78,7 +78,8 @@ fun formatMessageAsAnnotatedString( fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium )) val nicknameStart = builder.length - builder.append(baseName) + val truncatedBase = truncateNickname(baseName) + builder.append(truncatedBase) val nicknameEnd = builder.length // Add click annotation for nickname (store canonical sender name with hash if available) @@ -332,13 +333,13 @@ private fun appendIOSFormattedContent( builder.append("@") builder.pop() - // Base name + // Base name (truncate for rendering) builder.pushStyle(SpanStyle( color = mentionColor, fontSize = BASE_FONT_SIZE.sp, fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold )) - builder.append(mBase) + builder.append(truncateNickname(mBase)) builder.pop() // Hashtag suffix in lighter color diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt index 9265e3d1..5ce03bf1 100644 --- a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt +++ b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bitchat.android.ui.theme.BASE_FONT_SIZE import java.util.* +import androidx.compose.ui.text.style.TextOverflow /** * GeohashPeopleList - iOS-compatible component for displaying geohash participants @@ -115,6 +116,16 @@ fun GeohashPeopleList( } } } + + // Compute base name collisions to decide whether to show hash suffix + val baseNameCounts = remember(geohashPeople) { + val counts = mutableMapOf() + geohashPeople.forEach { person -> + val (b, _) = com.bitchat.android.ui.splitSuffix(person.displayName) + counts[b] = (counts[b] ?: 0) + 1 + } + counts + } val firstID = orderedPeople.firstOrNull()?.id @@ -129,6 +140,7 @@ fun GeohashPeopleList( nickname = nickname, colorScheme = colorScheme, viewModel = viewModel, + showHashSuffix = (baseNameCounts[com.bitchat.android.ui.splitSuffix(person.displayName).first] ?: 0) > 1, onTap = { if (person.id != myHex) { // TODO: Re-enable when NIP-17 geohash DM issues are fixed @@ -154,6 +166,7 @@ private fun GeohashPersonItem( nickname: String, colorScheme: ColorScheme, viewModel: ChatViewModel, + showHashSuffix: Boolean, onTap: () -> Unit ) { Row( @@ -198,8 +211,10 @@ private fun GeohashPersonItem( Spacer(modifier = Modifier.width(8.dp)) - // Display name with suffix handling (matches iOS splitSuffix logic) - val (baseName, suffix) = com.bitchat.android.ui.splitSuffix(person.displayName) + // Display name with suffix handling + val (baseNameRaw, suffixRaw) = com.bitchat.android.ui.splitSuffix(person.displayName) + val baseName = truncateNickname(baseNameRaw) + val suffix = if (showHashSuffix) suffixRaw else "" // Get consistent peer color (matches iOS color assignment exactly) val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f @@ -218,7 +233,9 @@ private fun GeohashPersonItem( fontSize = BASE_FONT_SIZE.sp, fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal ), - color = baseColor + color = baseColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) // Suffix (collision-resistant #abcd) in lighter shade diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 6c8c7917..2823896c 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.style.TextOverflow import com.bitchat.android.ui.theme.BASE_FONT_SIZE @@ -319,6 +320,52 @@ fun PeopleSection( .thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical ) + // Build a map of base name counts across all people shown in the list (connected + offline + nostr) + val hex64Regex = Regex("^[0-9a-fA-F]{64}$") + + // Helper to compute display name used for a given key + fun computeDisplayNameForPeerId(key: String): String { + return if (key == nickname) "You" else (peerNicknames[key] ?: (privateChats[key]?.lastOrNull()?.sender ?: key.take(12))) + } + + + + val baseNameCounts = mutableMapOf() + + // Connected peers + sortedPeers.forEach { pid -> + val dn = computeDisplayNameForPeerId(pid) + val (b, _) = com.bitchat.android.ui.splitSuffix(dn) + if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + } + + // Offline favorites (exclude ones mapped to connected) + val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites() + offlineFavorites.forEach { fav -> + val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) } + val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) } + if (!isMappedToConnected) { + val dn = peerNicknames[favPeerID] ?: fav.peerNickname + val (b, _) = com.bitchat.android.ui.splitSuffix(dn) + if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + } + } + + // Nostr-only conversations + val connectedIds = sortedPeers.toSet() + val appendedOfflineIds = mutableSetOf() + privateChats.keys + .filter { key -> + (key.startsWith("nostr_") || hex64Regex.matches(key)) && + !connectedIds.contains(key) && + !noiseHexByPeerID.values.any { it.equals(key, ignoreCase = true) } + } + .forEach { convKey -> + val dn = peerNicknames[convKey] ?: (privateChats[convKey]?.lastOrNull()?.sender ?: convKey.take(12)) + val (b, _) = com.bitchat.android.ui.splitSuffix(dn) + if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + } + sortedPeers.forEach { peerID -> val isFavorite = peerFavoriteStates[peerID] ?: false // fingerprint and favorite relationship resolution not needed here; UI will show Nostr globe for appended offline favorites below @@ -333,9 +380,13 @@ fun PeopleSection( if (noiseHex != null) privateChats[noiseHex]?.count { msg -> msg.sender != nickname && nostrUnread } ?: 0 else 0 ) + val displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: (privateChats[peerID]?.lastOrNull()?.sender ?: peerID.take(12))) + val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName) + val showHash = (baseNameCounts[bName] ?: 0) > 1 + PeerItem( peerID = peerID, - displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: (privateChats[peerID]?.lastOrNull()?.sender ?: peerID.take(12))), + displayName = displayName, signalStrength = convertRSSIToSignalStrength(peerRSSI[peerID]), isSelected = peerID == selectedPrivatePeer, isFavorite = isFavorite, @@ -348,13 +399,12 @@ fun PeopleSection( viewModel.toggleFavorite(peerID) }, unreadCount = if (combinedUnreadCount > 0) combinedUnreadCount else if (combinedHasUnread) 1 else 0, - showNostrGlobe = false + showNostrGlobe = false, + showHashSuffix = showHash ) } // Append offline favorites we actively favorite (and not currently connected) - val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites() - val appendedOfflineIds = mutableSetOf() offlineFavorites.forEach { fav -> val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) } // If any connected peer maps to this noise key, skip showing the offline entry @@ -364,9 +414,13 @@ fun PeopleSection( // If user clicks an offline favorite and the mapped peer is currently connected under a different ID, // open chat with the connected peerID instead of the noise hex for a seamless window val mappedConnectedPeerID = noiseHexByPeerID.entries.firstOrNull { it.value.equals(favPeerID, ignoreCase = true) }?.key + val dn = peerNicknames[favPeerID] ?: fav.peerNickname + val (bName, _) = com.bitchat.android.ui.splitSuffix(dn) + val showHash = (baseNameCounts[bName] ?: 0) > 1 + PeerItem( peerID = favPeerID, - displayName = peerNicknames[favPeerID] ?: fav.peerNickname, + displayName = dn, signalStrength = 0, isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer, isFavorite = true, @@ -381,16 +435,15 @@ fun PeopleSection( unreadCount = privateChats[favPeerID]?.count { msg -> msg.sender != nickname && hasUnreadPrivateMessages.contains(favPeerID) } ?: if (hasUnreadPrivateMessages.contains(favPeerID)) 1 else 0, - showNostrGlobe = (fav.isMutual && fav.peerNostrPublicKey != null) + showNostrGlobe = (fav.isMutual && fav.peerNostrPublicKey != null), + showHashSuffix = showHash ) appendedOfflineIds.add(favPeerID) } // Also show any incoming Nostr chats that exist locally but are not in connected peers or favorites yet // This ensures a user can open and read Nostr messages while the sender remains offline - val connectedIds = sortedPeers.toSet() val alreadyShownIds = connectedIds + appendedOfflineIds - val hex64Regex = Regex("^[0-9a-fA-F]{64}$") privateChats.keys .filter { key -> (key.startsWith("nostr_") || hex64Regex.matches(key)) && @@ -401,9 +454,13 @@ fun PeopleSection( .sortedBy { key -> privateChats[key]?.lastOrNull()?.timestamp } .forEach { convKey -> val lastSender = privateChats[convKey]?.lastOrNull()?.sender + val dn = peerNicknames[convKey] ?: (lastSender ?: convKey.take(12)) + val (bName, _) = com.bitchat.android.ui.splitSuffix(dn) + val showHash = (baseNameCounts[bName] ?: 0) > 1 + PeerItem( peerID = convKey, - displayName = peerNicknames[convKey] ?: (lastSender ?: convKey.take(12)), + displayName = dn, signalStrength = 0, isSelected = convKey == selectedPrivatePeer, isFavorite = false, @@ -415,7 +472,8 @@ fun PeopleSection( unreadCount = privateChats[convKey]?.count { msg -> msg.sender != nickname && hasUnreadPrivateMessages.contains(convKey) } ?: if (hasUnreadPrivateMessages.contains(convKey)) 1 else 0, - showNostrGlobe = true + showNostrGlobe = true, + showHashSuffix = showHash ) } } @@ -434,10 +492,13 @@ private fun PeerItem( onItemClick: () -> Unit, onToggleFavorite: () -> Unit, unreadCount: Int = 0, - showNostrGlobe: Boolean = false + showNostrGlobe: Boolean = false, + showHashSuffix: Boolean = true ) { // Split display name for hashtag suffix support (iOS-compatible) - val (baseName, suffix) = com.bitchat.android.ui.splitSuffix(displayName) + val (baseNameRaw, suffixRaw) = com.bitchat.android.ui.splitSuffix(displayName) + val baseName = truncateNickname(baseNameRaw) + val suffix = if (showHashSuffix) suffixRaw else "" val isMe = displayName == "You" || peerID == viewModel.nickname.value // Get consistent peer color (iOS-compatible) @@ -498,7 +559,9 @@ private fun PeerItem( fontSize = BASE_FONT_SIZE.sp, fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal ), - color = baseColor + color = baseColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) // Hashtag suffix in lighter shade (iOS-style)