diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt index 4f443c4b..01ce3060 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt @@ -1302,11 +1302,29 @@ class NostrGeohashService( Log.d(TAG, "🔔 Triggering geohash notification - geohash: $geohash, mention: $isMention, first: $isFirstMessage") withContext(Dispatchers.Main) { + // Sanitize mention for notifications: hide '#hash' from @username#hash if not needed + val contentForNotification = if (isMention) { + var sanitized = content + try { + val myGeoIdentity = NostrIdentityBridge.deriveIdentity( + forGeohash = geohash, + context = application + ) + val myDisplay = displayNameForNostrPubkeyUI(myGeoIdentity.publicKeyHex) + val needsDisambiguation = myDisplay.contains("#") + if (!needsDisambiguation) { + val pattern = ("@" + java.util.regex.Pattern.quote(currentNickname) + "#[a-fA-F0-9]{4}\\b").toRegex() + sanitized = sanitized.replace(pattern, "@" + currentNickname) + } + } catch (_: Exception) { } + sanitized + } else content + val locationName = getLocationNameForGeohash(geohash) notificationManager.showGeohashNotification( geohash = geohash, senderNickname = senderName, - messageContent = content, + messageContent = contentForNotification, isMention = isMention, isFirstMessage = isFirstMessage, locationName = locationName 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 14ed6a7b..d1e34307 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp import com.bitchat.android.model.BitchatMessage import com.bitchat.android.mesh.BluetoothMeshService @@ -257,6 +258,31 @@ private fun appendIOSFormattedContent( for (match in mentionMatches) { allMatches.add(match.range to "mention") } + + // Add standalone geohash matches (e.g., "#9q") that are not part of another word + // We use MessageSpecialParser to find exact ranges; then merge with existing ranges avoiding overlaps + val geoMatches = MessageSpecialParser.findStandaloneGeohashes(content) + for (gm in geoMatches) { + val range = gm.start until gm.endExclusive + if (!overlapsMention(range)) { + allMatches.add(range to "geohash") + } + } + + // Remove generic hashtag matches that overlap with detected geohash ranges to avoid duplicate rendering + fun rangesOverlap(a: IntRange, b: IntRange): Boolean { + return a.first < b.last && a.last > b.first + } + val geoRanges = allMatches.filter { it.second == "geohash" }.map { it.first } + if (geoRanges.isNotEmpty()) { + val iterator = allMatches.listIterator() + while (iterator.hasNext()) { + val (range, type) = iterator.next() + if (type == "hashtag" && geoRanges.any { rangesOverlap(range, it) }) { + iterator.remove() + } + } + } allMatches.sortBy { it.first.first } @@ -327,7 +353,7 @@ private fun appendIOSFormattedContent( } } "hashtag" -> { - // iOS-style: render hashtags like normal content (no special styling) + // Render general hashtags like normal content builder.pushStyle(SpanStyle( color = baseColor, fontSize = BASE_FONT_SIZE.sp, @@ -342,6 +368,37 @@ private fun appendIOSFormattedContent( } builder.pop() } + else -> { + if (type == "geohash") { + // Style geohash in blue, underlined, and add click annotation + builder.pushStyle(SpanStyle( + color = Color(0xFF007AFF), + fontSize = BASE_FONT_SIZE.sp, + fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold, + textDecoration = TextDecoration.Underline + )) + val start = builder.length + builder.append(matchText) + val end = builder.length + val geohash = matchText.removePrefix("#").lowercase() + builder.addStringAnnotation( + tag = "geohash_click", + annotation = geohash, + start = start, + end = end + ) + builder.pop() + } else { + // Fallback: treat as normal text + builder.pushStyle(SpanStyle( + color = baseColor, + fontSize = BASE_FONT_SIZE.sp, + fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal + )) + builder.append(matchText) + builder.pop() + } + } } lastEnd = range.last + 1 diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index b2e494a6..a6cbe8e9 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import com.bitchat.android.model.BitchatMessage @@ -199,6 +200,7 @@ private fun MessageTextWithClickableNicknames( if (!isSelf && (onNicknameClick != null || onMessageLongPress != null)) { val haptic = LocalHapticFeedback.current + val context = LocalContext.current var textLayoutResult by remember { mutableStateOf(null) } Text( text = annotatedText, @@ -216,6 +218,33 @@ private fun MessageTextWithClickableNicknames( val nickname = nicknameAnnotations.first().item haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) onNicknameClick?.invoke(nickname) + return@detectTapGestures + } + // Handle geohash click to teleport + val geohashAnnotations = annotatedText.getStringAnnotations( + tag = "geohash_click", + start = offset, + end = offset + ) + if (geohashAnnotations.isNotEmpty()) { + val geohash = geohashAnnotations.first().item + try { + val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance( + context + ) + // Select appropriate precision based on length + val level = when (geohash.length) { + in 0..2 -> com.bitchat.android.geohash.GeohashChannelLevel.REGION + in 3..4 -> com.bitchat.android.geohash.GeohashChannelLevel.PROVINCE + 5 -> com.bitchat.android.geohash.GeohashChannelLevel.CITY + 6 -> com.bitchat.android.geohash.GeohashChannelLevel.NEIGHBORHOOD + else -> com.bitchat.android.geohash.GeohashChannelLevel.BLOCK + } + val channel = com.bitchat.android.geohash.GeohashChannel(level, geohash.lowercase()) + locationManager.setTeleported(true) + locationManager.select(com.bitchat.android.geohash.ChannelID.Location(channel)) + } catch (_: Exception) { } + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) } }, onLongPress = {