mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
parser
This commit is contained in:
@@ -130,6 +130,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Messages area - takes up available space, will compress when keyboard appears
|
// Messages area - takes up available space, will compress when keyboard appears
|
||||||
|
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||||
MessagesList(
|
MessagesList(
|
||||||
messages = displayMessages,
|
messages = displayMessages,
|
||||||
currentUserNickname = nickname,
|
currentUserNickname = nickname,
|
||||||
@@ -137,6 +138,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
forceScrollToBottom = forceScrollToBottom,
|
forceScrollToBottom = forceScrollToBottom,
|
||||||
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||||
|
peerNicknames = peerNicknames,
|
||||||
onNicknameClick = { fullSenderName ->
|
onNicknameClick = { fullSenderName ->
|
||||||
// Single click - mention user in text input
|
// Single click - mention user in text input
|
||||||
val currentText = messageText.text
|
val currentText = messageText.text
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ fun formatMessageAsAnnotatedString(
|
|||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: BluetoothMeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()),
|
||||||
|
peerNicknames: Map<String, String> = emptyMap()
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
val builder = AnnotatedString.Builder()
|
val builder = AnnotatedString.Builder()
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||||
@@ -53,26 +54,26 @@ 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
|
// Resolve disambiguated sender name if it's a mesh message
|
||||||
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
||||||
val disambiguated = meshService.getDisambiguatedNickname(message.senderPeerID)
|
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 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) {
|
if (disambiguated == message.senderPeerID && message.sender != message.senderPeerID) {
|
||||||
message.sender
|
message.sender
|
||||||
} else {
|
} else {
|
||||||
disambiguated
|
disambiguated
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message.sender
|
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)
|
||||||
} else {
|
} else {
|
||||||
getPeerColor(message, isDark)
|
colorForPeer(message.senderPeerID, message.sender, isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split sender into base name and hashtag suffix
|
// Split sender into base name and hashtag suffix
|
||||||
@@ -130,10 +131,9 @@ fun formatMessageAsAnnotatedString(
|
|||||||
builder.pop()
|
builder.pop()
|
||||||
|
|
||||||
// Message content with iOS-style hashtag and mention highlighting
|
// Message content with iOS-style hashtag and mention highlighting
|
||||||
appendIOSFormattedContent(builder, message.content, message.mentions, currentUserNickname, baseColor, isSelf, isDark)
|
appendIOSFormattedContent(builder, message.content, message.mentions, currentUserNickname, baseColor, isSelf, isDark, meshService, peerNicknames)
|
||||||
|
|
||||||
// iOS-style timestamp at the END (smaller, grey)
|
// iOS-style timestamp at the END (smaller, grey)
|
||||||
// Timestamp (and optional PoW badge)
|
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color.Gray.copy(alpha = 0.7f),
|
color = Color.Gray.copy(alpha = 0.7f),
|
||||||
fontSize = (BASE_FONT_SIZE - 4).sp
|
fontSize = (BASE_FONT_SIZE - 4).sp
|
||||||
@@ -152,7 +152,7 @@ fun formatMessageAsAnnotatedString(
|
|||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
fontSize = (BASE_FONT_SIZE - 2).sp,
|
fontSize = (BASE_FONT_SIZE - 2).sp,
|
||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
fontStyle = FontStyle.Italic
|
||||||
))
|
))
|
||||||
builder.append("* ${message.content} *")
|
builder.append("* ${message.content} *")
|
||||||
builder.pop()
|
builder.pop()
|
||||||
@@ -177,7 +177,8 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: BluetoothMeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()),
|
||||||
|
peerNicknames: Map<String, String> = emptyMap()
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
val builder = AnnotatedString.Builder()
|
val builder = AnnotatedString.Builder()
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||||
@@ -189,18 +190,12 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
if (message.sender != "system") {
|
if (message.sender != "system") {
|
||||||
// Resolve disambiguated sender name if it's a mesh message
|
// Resolve disambiguated sender name if it's a mesh message
|
||||||
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
val senderDisplayName = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
|
||||||
val disambiguated = meshService.getDisambiguatedNickname(message.senderPeerID)
|
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 {
|
} else {
|
||||||
message.sender
|
message.sender
|
||||||
}
|
}
|
||||||
|
|
||||||
val baseColor = if (isSelf) Color(0xFFFF9500) else getPeerColor(message, isDark)
|
val baseColor = if (isSelf) Color(0xFFFF9500) else colorForPeer(message.senderPeerID, message.sender, isDark)
|
||||||
val (baseName, suffix) = splitSuffix(senderDisplayName)
|
val (baseName, suffix) = splitSuffix(senderDisplayName)
|
||||||
|
|
||||||
// "<@"
|
// "<@"
|
||||||
@@ -266,7 +261,7 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
fontSize = (BASE_FONT_SIZE - 2).sp,
|
fontSize = (BASE_FONT_SIZE - 2).sp,
|
||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
fontStyle = FontStyle.Italic
|
||||||
))
|
))
|
||||||
builder.append("* ${message.content} *")
|
builder.append("* ${message.content} *")
|
||||||
builder.pop()
|
builder.pop()
|
||||||
@@ -286,23 +281,26 @@ fun formatMessageHeaderAnnotatedString(
|
|||||||
* Avoids orange (~30°) reserved for self messages
|
* Avoids orange (~30°) reserved for self messages
|
||||||
*/
|
*/
|
||||||
fun getPeerColor(message: BitchatMessage, isDark: Boolean): Color {
|
fun getPeerColor(message: BitchatMessage, isDark: Boolean): Color {
|
||||||
|
return colorForPeer(message.senderPeerID, message.sender, isDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate consistent peer color based on peer ID or nickname
|
||||||
|
*/
|
||||||
|
fun colorForPeer(peerID: String?, nickname: String, isDark: Boolean): Color {
|
||||||
// Create seed from peer identifier (prioritizing stable keys)
|
// Create seed from peer identifier (prioritizing stable keys)
|
||||||
val seed = when {
|
val seed = when {
|
||||||
message.senderPeerID?.startsWith("nostr:") == true || message.senderPeerID?.startsWith("nostr_") == true -> {
|
peerID?.startsWith("nostr:") == true || peerID?.startsWith("nostr_") == true -> {
|
||||||
// For Nostr peers, use the full key if available, otherwise the peer ID
|
"nostr:${peerID.lowercase()}"
|
||||||
"nostr:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
}
|
||||||
message.senderPeerID?.length == 16 -> {
|
peerID?.length == 16 -> {
|
||||||
// For ephemeral peer IDs, try to get stable Noise key, fallback to peer ID
|
"noise:${peerID.lowercase()}"
|
||||||
"noise:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
}
|
||||||
message.senderPeerID?.length == 64 -> {
|
peerID?.length == 64 -> {
|
||||||
// This is already a stable Noise key
|
"noise:${peerID.lowercase()}"
|
||||||
"noise:${message.senderPeerID.lowercase()}"
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Fallback to sender name
|
nickname.lowercase()
|
||||||
message.sender.lowercase()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +352,34 @@ fun splitSuffix(name: String): Pair<String, String> {
|
|||||||
return Pair(name, "")
|
return Pair(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate nickname if it's too long for UI display
|
||||||
|
*/
|
||||||
|
fun truncateNickname(nickname: String): String {
|
||||||
|
return if (nickname.length > 20) {
|
||||||
|
nickname.take(18) + ".."
|
||||||
|
} else {
|
||||||
|
nickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a display name (with potential #suffix) back to a Peer ID
|
||||||
|
*/
|
||||||
|
fun resolvePeerIDFromDisplayName(displayName: String, nicknames: Map<String, String>): String? {
|
||||||
|
val (base, suffix) = splitSuffix(displayName)
|
||||||
|
|
||||||
|
return nicknames.entries.find { (id, nick) ->
|
||||||
|
if (suffix.isNotEmpty()) {
|
||||||
|
// Match nickname and Peer ID ending
|
||||||
|
nick.equals(base, ignoreCase = true) && id.endsWith(suffix.removePrefix("#"), ignoreCase = true)
|
||||||
|
} else {
|
||||||
|
// Match nickname only
|
||||||
|
nick.equals(displayName, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}?.key
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* iOS-style content formatting with proper hashtag and mention handling
|
* iOS-style content formatting with proper hashtag and mention handling
|
||||||
*/
|
*/
|
||||||
@@ -364,7 +390,9 @@ private fun appendIOSFormattedContent(
|
|||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
baseColor: Color,
|
baseColor: Color,
|
||||||
isSelf: Boolean,
|
isSelf: Boolean,
|
||||||
isDark: Boolean
|
isDark: Boolean,
|
||||||
|
meshService: BluetoothMeshService,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap()
|
||||||
) {
|
) {
|
||||||
// iOS-style patterns: allow optional '#abcd' suffix in mentions
|
// iOS-style patterns: allow optional '#abcd' suffix in mentions
|
||||||
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
|
val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex()
|
||||||
@@ -396,7 +424,6 @@ private fun appendIOSFormattedContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add standalone geohash matches (e.g., "#9q") that are not part of another word
|
// 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)
|
val geoMatches = MessageSpecialParser.findStandaloneGeohashes(content)
|
||||||
for (gm in geoMatches) {
|
for (gm in geoMatches) {
|
||||||
val range = gm.start until gm.endExclusive
|
val range = gm.start until gm.endExclusive
|
||||||
@@ -414,7 +441,7 @@ private fun appendIOSFormattedContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove generic hashtag matches that overlap with detected geohash ranges to avoid duplicate rendering
|
// Remove generic hashtag matches that overlap with detected geohash ranges
|
||||||
fun rangesOverlap(a: IntRange, b: IntRange): Boolean {
|
fun rangesOverlap(a: IntRange, b: IntRange): Boolean {
|
||||||
return a.first < b.last && a.last > b.first
|
return a.first < b.last && a.last > b.first
|
||||||
}
|
}
|
||||||
@@ -424,7 +451,6 @@ private fun appendIOSFormattedContent(
|
|||||||
val iterator = allMatches.listIterator()
|
val iterator = allMatches.listIterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val (range, type) = iterator.next()
|
val (range, type) = iterator.next()
|
||||||
// Remove generic hashtags that overlap with geohashes, and geohashes that overlap with URLs
|
|
||||||
val overlapsGeo = geoRanges.any { rangesOverlap(range, it) }
|
val overlapsGeo = geoRanges.any { rangesOverlap(range, it) }
|
||||||
val overlapsUrl = urlRanges.any { rangesOverlap(range, it) }
|
val overlapsUrl = urlRanges.any { rangesOverlap(range, it) }
|
||||||
if ((type == "hashtag" && overlapsGeo) || (type == "geohash" && overlapsUrl)) iterator.remove()
|
if ((type == "hashtag" && overlapsGeo) || (type == "geohash" && overlapsUrl)) iterator.remove()
|
||||||
@@ -436,6 +462,9 @@ private fun appendIOSFormattedContent(
|
|||||||
var lastEnd = 0
|
var lastEnd = 0
|
||||||
val isMentioned = mentions?.contains(currentUserNickname) == true
|
val isMentioned = mentions?.contains(currentUserNickname) == true
|
||||||
|
|
||||||
|
// Use provided map if available, else query service once
|
||||||
|
val nicknameMap = if (peerNicknames.isNotEmpty()) peerNicknames else meshService.getPeerNicknames()
|
||||||
|
|
||||||
for ((range, type) in allMatches) {
|
for ((range, type) in allMatches) {
|
||||||
// Add text before match
|
// Add text before match
|
||||||
if (lastEnd < range.first) {
|
if (lastEnd < range.first) {
|
||||||
@@ -447,7 +476,6 @@ private fun appendIOSFormattedContent(
|
|||||||
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal
|
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal
|
||||||
))
|
))
|
||||||
if (isMentioned) {
|
if (isMentioned) {
|
||||||
// Make entire message bold if user is mentioned
|
|
||||||
builder.pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
|
builder.pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
|
||||||
builder.append(beforeText)
|
builder.append(beforeText)
|
||||||
builder.pop()
|
builder.pop()
|
||||||
@@ -462,13 +490,19 @@ private fun appendIOSFormattedContent(
|
|||||||
val matchText = content.substring(range.first, range.last + 1)
|
val matchText = content.substring(range.first, range.last + 1)
|
||||||
when (type) {
|
when (type) {
|
||||||
"mention" -> {
|
"mention" -> {
|
||||||
// iOS-style mention with hashtag suffix support
|
|
||||||
val mentionWithoutAt = matchText.removePrefix("@")
|
val mentionWithoutAt = matchText.removePrefix("@")
|
||||||
val (mBase, mSuffix) = splitSuffix(mentionWithoutAt)
|
val (mBase, mSuffix) = splitSuffix(mentionWithoutAt)
|
||||||
|
|
||||||
// Check if this mention targets current user
|
// Check if this mention targets current user
|
||||||
val isMentionToMe = mBase == currentUserNickname
|
val isMentionToMe = mBase.equals(currentUserNickname, ignoreCase = true)
|
||||||
val mentionColor = if (isMentionToMe) Color(0xFFFF9500) else baseColor
|
|
||||||
|
// Resolve mention color based on mentioned user's actual color
|
||||||
|
val mentionColor = if (isMentionToMe) {
|
||||||
|
Color(0xFFFF9500) // Orange for mentions to self
|
||||||
|
} else {
|
||||||
|
val resolvedID = resolvePeerIDFromDisplayName(mentionWithoutAt, nicknameMap)
|
||||||
|
colorForPeer(resolvedID, mentionWithoutAt, isDark)
|
||||||
|
}
|
||||||
|
|
||||||
// "@" symbol
|
// "@" symbol
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
@@ -479,7 +513,7 @@ private fun appendIOSFormattedContent(
|
|||||||
builder.append("@")
|
builder.append("@")
|
||||||
builder.pop()
|
builder.pop()
|
||||||
|
|
||||||
// Base name (truncate for rendering)
|
// Base name
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = mentionColor,
|
color = mentionColor,
|
||||||
fontSize = BASE_FONT_SIZE.sp,
|
fontSize = BASE_FONT_SIZE.sp,
|
||||||
@@ -500,7 +534,6 @@ private fun appendIOSFormattedContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"hashtag" -> {
|
"hashtag" -> {
|
||||||
// Render general hashtags like normal content
|
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = baseColor,
|
color = baseColor,
|
||||||
fontSize = BASE_FONT_SIZE.sp,
|
fontSize = BASE_FONT_SIZE.sp,
|
||||||
@@ -517,7 +550,6 @@ private fun appendIOSFormattedContent(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (type == "geohash") {
|
if (type == "geohash") {
|
||||||
// Style geohash in blue, underlined, and add click annotation
|
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color(0xFF007AFF),
|
color = Color(0xFF007AFF),
|
||||||
fontSize = BASE_FONT_SIZE.sp,
|
fontSize = BASE_FONT_SIZE.sp,
|
||||||
@@ -528,15 +560,9 @@ private fun appendIOSFormattedContent(
|
|||||||
builder.append(matchText)
|
builder.append(matchText)
|
||||||
val end = builder.length
|
val end = builder.length
|
||||||
val geohash = matchText.removePrefix("#").lowercase()
|
val geohash = matchText.removePrefix("#").lowercase()
|
||||||
builder.addStringAnnotation(
|
builder.addStringAnnotation(tag = "geohash_click", annotation = geohash, start = start, end = end)
|
||||||
tag = "geohash_click",
|
|
||||||
annotation = geohash,
|
|
||||||
start = start,
|
|
||||||
end = end
|
|
||||||
)
|
|
||||||
builder.pop()
|
builder.pop()
|
||||||
} else if (type == "url") {
|
} else if (type == "url") {
|
||||||
// Style URL in blue, underlined, and add click annotation with the raw text
|
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color(0xFF007AFF),
|
color = Color(0xFF007AFF),
|
||||||
fontSize = BASE_FONT_SIZE.sp,
|
fontSize = BASE_FONT_SIZE.sp,
|
||||||
@@ -546,15 +572,9 @@ private fun appendIOSFormattedContent(
|
|||||||
val start = builder.length
|
val start = builder.length
|
||||||
builder.append(matchText)
|
builder.append(matchText)
|
||||||
val end = builder.length
|
val end = builder.length
|
||||||
builder.addStringAnnotation(
|
builder.addStringAnnotation(tag = "url_click", annotation = matchText, start = start, end = end)
|
||||||
tag = "url_click",
|
|
||||||
annotation = matchText,
|
|
||||||
start = start,
|
|
||||||
end = end
|
|
||||||
)
|
|
||||||
builder.pop()
|
builder.pop()
|
||||||
} else {
|
} else {
|
||||||
// Fallback: treat as normal text
|
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = baseColor,
|
color = baseColor,
|
||||||
fontSize = BASE_FONT_SIZE.sp,
|
fontSize = BASE_FONT_SIZE.sp,
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ private fun formatMessageAsAnnotatedStringWithoutTimestamp(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
meshService = meshService,
|
meshService = meshService,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter,
|
||||||
|
peerNicknames = emptyMap() // Can't easily get map here without refactoring AnimatedMessageDisplay
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find and remove the timestamp and PoW badge at the end
|
// Find and remove the timestamp and PoW badge at the end
|
||||||
|
|||||||
@@ -839,7 +839,8 @@ fun PrivateChatSheet(
|
|||||||
onNicknameClick = { /* handle mention */ },
|
onNicknameClick = { /* handle mention */ },
|
||||||
onMessageLongPress = { /* handle long press */ },
|
onMessageLongPress = { /* handle long press */ },
|
||||||
onCancelTransfer = { msg -> viewModel.cancelMediaSend(msg.id) },
|
onCancelTransfer = { msg -> viewModel.cancelMediaSend(msg.id) },
|
||||||
onImageClick = { _, _, _ -> /* handle image click */ }
|
onImageClick = { _, _, _ -> /* handle image click */ },
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f))
|
HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f))
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ fun MessagesList(
|
|||||||
onNicknameClick: ((String) -> Unit)? = null,
|
onNicknameClick: ((String) -> Unit)? = null,
|
||||||
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
||||||
onCancelTransfer: ((BitchatMessage) -> Unit)? = null,
|
onCancelTransfer: ((BitchatMessage) -> Unit)? = null,
|
||||||
onImageClick: ((String, List<String>, Int) -> Unit)? = null
|
onImageClick: ((String, List<String>, Int) -> Unit)? = null,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap()
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -126,7 +127,8 @@ fun MessagesList(
|
|||||||
onNicknameClick = onNicknameClick,
|
onNicknameClick = onNicknameClick,
|
||||||
onMessageLongPress = onMessageLongPress,
|
onMessageLongPress = onMessageLongPress,
|
||||||
onCancelTransfer = onCancelTransfer,
|
onCancelTransfer = onCancelTransfer,
|
||||||
onImageClick = onImageClick
|
onImageClick = onImageClick,
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +144,8 @@ fun MessageItem(
|
|||||||
onNicknameClick: ((String) -> Unit)? = null,
|
onNicknameClick: ((String) -> Unit)? = null,
|
||||||
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
||||||
onCancelTransfer: ((BitchatMessage) -> Unit)? = null,
|
onCancelTransfer: ((BitchatMessage) -> Unit)? = null,
|
||||||
onImageClick: ((String, List<String>, Int) -> Unit)? = null
|
onImageClick: ((String, List<String>, Int) -> Unit)? = null,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap()
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
|
val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
|
||||||
@@ -171,6 +174,7 @@ fun MessageItem(
|
|||||||
onMessageLongPress = onMessageLongPress,
|
onMessageLongPress = onMessageLongPress,
|
||||||
onCancelTransfer = onCancelTransfer,
|
onCancelTransfer = onCancelTransfer,
|
||||||
onImageClick = onImageClick,
|
onImageClick = onImageClick,
|
||||||
|
peerNicknames = peerNicknames,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(end = endPad)
|
.padding(end = endPad)
|
||||||
@@ -208,6 +212,7 @@ fun MessageItem(
|
|||||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||||
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
||||||
onImageClick: ((String, List<String>, Int) -> Unit)?,
|
onImageClick: ((String, List<String>, Int) -> Unit)?,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// Image special rendering
|
// Image special rendering
|
||||||
@@ -223,6 +228,7 @@ fun MessageItem(
|
|||||||
onMessageLongPress = onMessageLongPress,
|
onMessageLongPress = onMessageLongPress,
|
||||||
onCancelTransfer = onCancelTransfer,
|
onCancelTransfer = onCancelTransfer,
|
||||||
onImageClick = onImageClick,
|
onImageClick = onImageClick,
|
||||||
|
peerNicknames = peerNicknames,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -239,6 +245,7 @@ fun MessageItem(
|
|||||||
onNicknameClick = onNicknameClick,
|
onNicknameClick = onNicknameClick,
|
||||||
onMessageLongPress = onMessageLongPress,
|
onMessageLongPress = onMessageLongPress,
|
||||||
onCancelTransfer = onCancelTransfer,
|
onCancelTransfer = onCancelTransfer,
|
||||||
|
peerNicknames = peerNicknames,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -263,7 +270,8 @@ fun MessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
meshService = meshService,
|
meshService = meshService,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter,
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
@@ -371,7 +379,8 @@ fun MessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
meshService = meshService,
|
meshService = meshService,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter,
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if this message was sent by self to avoid click interactions on own nickname
|
// Check if this message was sent by self to avoid click interactions on own nickname
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ fun AudioMessageItem(
|
|||||||
onNicknameClick: ((String) -> Unit)?,
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||||
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val path = message.content.trim()
|
val path = message.content.trim()
|
||||||
@@ -55,7 +56,8 @@ fun AudioMessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
meshService = meshService,
|
meshService = meshService,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter,
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ fun ImageMessageItem(
|
|||||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||||
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
||||||
onImageClick: ((String, List<String>, Int) -> Unit)?,
|
onImageClick: ((String, List<String>, Int) -> Unit)?,
|
||||||
|
peerNicknames: Map<String, String> = emptyMap(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val path = message.content.trim()
|
val path = message.content.trim()
|
||||||
@@ -55,7 +56,8 @@ fun ImageMessageItem(
|
|||||||
currentUserNickname = currentUserNickname,
|
currentUserNickname = currentUserNickname,
|
||||||
meshService = meshService,
|
meshService = meshService,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
timeFormatter = timeFormatter
|
timeFormatter = timeFormatter,
|
||||||
|
peerNicknames = peerNicknames
|
||||||
)
|
)
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
var headerLayout by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
|||||||
Reference in New Issue
Block a user