UI geohash notifications (#325)

* location name in notifications

* remove tests

* panic nostr

* mentions with hashes, otherwise none

* fix timestamps

* parse geohashes in messages

* works

* fix country name

* mention notifications work
This commit is contained in:
callebtc
2025-08-28 17:47:47 +02:00
committed by GitHub
parent 3ea2aed9a4
commit 550520795f
12 changed files with 393 additions and 34 deletions
@@ -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
@@ -80,11 +81,11 @@ fun formatMessageAsAnnotatedString(
builder.append(baseName)
val nicknameEnd = builder.length
// Add click annotation for nickname (store full sender name with hash)
// Add click annotation for nickname (store canonical sender name with hash if available)
if (!isSelf) {
builder.addStringAnnotation(
tag = "nickname_click",
annotation = message.sender, // Store full sender name with hash
annotation = (message.originalSender ?: message.sender),
start = nicknameStart,
end = nicknameEnd
)
@@ -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