mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 20:45:20 +00:00
matrix style
This commit is contained in:
@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for all Nostr and Geohash business logic extracted from ChatViewModel
|
* Service responsible for all Nostr and Geohash business logic extracted from ChatViewModel
|
||||||
@@ -242,6 +243,35 @@ class NostrGeohashService(
|
|||||||
fun sendGeohashMessage(content: String, channel: com.bitchat.android.geohash.GeohashChannel, myPeerID: String, nickname: String?) {
|
fun sendGeohashMessage(content: String, channel: com.bitchat.android.geohash.GeohashChannel, myPeerID: String, nickname: String?) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
|
// Generate a temporary message ID for tracking animation
|
||||||
|
val tempMessageId = "temp_${System.currentTimeMillis()}_${Random.nextInt(1000)}"
|
||||||
|
|
||||||
|
// Add local echo message IMMEDIATELY (with temporary ID)
|
||||||
|
val localMessage = BitchatMessage(
|
||||||
|
id = tempMessageId,
|
||||||
|
sender = nickname ?: myPeerID,
|
||||||
|
content = content,
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false,
|
||||||
|
senderPeerID = "geohash:${channel.geohash}",
|
||||||
|
channel = "#${channel.geohash}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store and display the message immediately
|
||||||
|
storeGeohashMessage(channel.geohash, localMessage)
|
||||||
|
messageManager.addMessage(localMessage)
|
||||||
|
|
||||||
|
Log.d(TAG, "📝 Added message immediately with temp ID: $tempMessageId")
|
||||||
|
|
||||||
|
// Check if PoW is enabled before starting animation
|
||||||
|
val powSettings = PoWPreferenceManager.getCurrentSettings()
|
||||||
|
if (powSettings.enabled && powSettings.difficulty > 0) {
|
||||||
|
// Start matrix animation for this message
|
||||||
|
com.bitchat.android.ui.PoWMiningTracker.startMiningMessage(tempMessageId)
|
||||||
|
Log.d(TAG, "🎭 Started matrix animation for message: $tempMessageId")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now begin the async PoW process
|
||||||
val identity = NostrIdentityBridge.deriveIdentity(
|
val identity = NostrIdentityBridge.deriveIdentity(
|
||||||
forGeohash = channel.geohash,
|
forGeohash = channel.geohash,
|
||||||
context = application
|
context = application
|
||||||
@@ -257,6 +287,12 @@ class NostrGeohashService(
|
|||||||
teleported = teleported
|
teleported = teleported
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Stop animation when PoW completes
|
||||||
|
if (powSettings.enabled && powSettings.difficulty > 0) {
|
||||||
|
com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage(tempMessageId)
|
||||||
|
Log.d(TAG, "🎭 Stopped matrix animation for message: $tempMessageId")
|
||||||
|
}
|
||||||
|
|
||||||
val nostrRelayManager = NostrRelayManager.getInstance(application)
|
val nostrRelayManager = NostrRelayManager.getInstance(application)
|
||||||
nostrRelayManager.sendEventToGeohash(
|
nostrRelayManager.sendEventToGeohash(
|
||||||
event = event,
|
event = event,
|
||||||
@@ -267,22 +303,10 @@ class NostrGeohashService(
|
|||||||
|
|
||||||
Log.i(TAG, "📤 Sent geohash message to ${channel.geohash}: ${content.take(50)}")
|
Log.i(TAG, "📤 Sent geohash message to ${channel.geohash}: ${content.take(50)}")
|
||||||
|
|
||||||
// Add local echo message
|
|
||||||
val localMessage = BitchatMessage(
|
|
||||||
sender = nickname ?: myPeerID,
|
|
||||||
content = content,
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false,
|
|
||||||
senderPeerID = "geohash:${channel.geohash}",
|
|
||||||
channel = "#${channel.geohash}"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store our own message in geohash history
|
|
||||||
storeGeohashMessage(channel.geohash, localMessage)
|
|
||||||
messageManager.addMessage(localMessage)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send geohash message: ${e.message}")
|
Log.e(TAG, "Failed to send geohash message: ${e.message}")
|
||||||
|
// Make sure to stop animation even if there's an error
|
||||||
|
com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage("temp_${System.currentTimeMillis()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix-style encryption animation for messages during PoW computation
|
||||||
|
* Animates message content with random characters that gradually resolve to real text
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AnimatedMatrixText(
|
||||||
|
targetText: String,
|
||||||
|
isAnimating: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
|
fontWeight: FontWeight? = null
|
||||||
|
) {
|
||||||
|
// Animation state for each character
|
||||||
|
var animatedText by remember(targetText) { mutableStateOf(targetText) }
|
||||||
|
var characterStates by remember(targetText) { mutableStateOf(
|
||||||
|
targetText.indices.map { CharacterAnimationState.ENCRYPTED }.toList()
|
||||||
|
) }
|
||||||
|
|
||||||
|
// Start animation when isAnimating becomes true
|
||||||
|
LaunchedEffect(isAnimating, targetText) {
|
||||||
|
if (isAnimating && targetText.isNotEmpty()) {
|
||||||
|
// Reset character states for new animation
|
||||||
|
characterStates = targetText.indices.map { CharacterAnimationState.ENCRYPTED }.toList()
|
||||||
|
|
||||||
|
// Animate all characters in parallel with staggered start times
|
||||||
|
targetText.forEachIndexed { index, targetChar ->
|
||||||
|
launch { // Use the LaunchedEffect's CoroutineScope
|
||||||
|
// Stagger character animations (50ms per character like the JS example)
|
||||||
|
delay(index * 50L)
|
||||||
|
|
||||||
|
animateCharacter(
|
||||||
|
index = index,
|
||||||
|
targetChar = targetChar,
|
||||||
|
currentText = { animatedText },
|
||||||
|
updateText = { newText -> animatedText = newText },
|
||||||
|
updateState = { newStates -> characterStates = newStates }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not animating, show final text immediately
|
||||||
|
animatedText = targetText
|
||||||
|
characterStates = targetText.indices.map { CharacterAnimationState.FINAL }.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build annotated string with proper styling for each character state
|
||||||
|
val styledText = buildAnnotatedString {
|
||||||
|
animatedText.forEachIndexed { index, char ->
|
||||||
|
val state = characterStates.getOrNull(index) ?: CharacterAnimationState.FINAL
|
||||||
|
val charColor = when (state) {
|
||||||
|
CharacterAnimationState.ENCRYPTED -> Color(0xFF00FF41) // Matrix green
|
||||||
|
CharacterAnimationState.DECRYPTING -> Color(0xFFFFFF41) // Yellow transition
|
||||||
|
CharacterAnimationState.FINAL -> color
|
||||||
|
}
|
||||||
|
|
||||||
|
withStyle(
|
||||||
|
style = SpanStyle(
|
||||||
|
color = charColor,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = if (state == CharacterAnimationState.ENCRYPTED) FontWeight.Bold else fontWeight
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = styledText,
|
||||||
|
modifier = modifier,
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation state for individual characters
|
||||||
|
*/
|
||||||
|
private enum class CharacterAnimationState {
|
||||||
|
ENCRYPTED, // Showing random encrypted characters
|
||||||
|
DECRYPTING, // Transitioning to final character
|
||||||
|
FINAL // Showing final decrypted character
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animate a single character from encrypted to final state
|
||||||
|
*/
|
||||||
|
private suspend fun animateCharacter(
|
||||||
|
index: Int,
|
||||||
|
targetChar: Char,
|
||||||
|
currentText: () -> String,
|
||||||
|
updateText: (String) -> Unit,
|
||||||
|
updateState: (List<CharacterAnimationState>) -> Unit
|
||||||
|
) {
|
||||||
|
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
|
||||||
|
val animationDuration = Random.nextLong(1000, 3000) // 1-3 seconds of encryption
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Phase 1: Animate with random encrypted characters
|
||||||
|
while (System.currentTimeMillis() - startTime < animationDuration) {
|
||||||
|
val randomChar = encryptedChars[Random.nextInt(encryptedChars.size)]
|
||||||
|
|
||||||
|
// Update the character at this index
|
||||||
|
val text = currentText()
|
||||||
|
if (index < text.length) {
|
||||||
|
val newText = text.toCharArray()
|
||||||
|
newText[index] = randomChar
|
||||||
|
updateText(String(newText))
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(100) // Change character every 100ms
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Brief transition state (yellow)
|
||||||
|
val states = currentText().indices.map { i ->
|
||||||
|
when {
|
||||||
|
i == index -> CharacterAnimationState.DECRYPTING
|
||||||
|
i < index -> CharacterAnimationState.FINAL // Already revealed
|
||||||
|
else -> CharacterAnimationState.ENCRYPTED // Still encrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateState(states)
|
||||||
|
delay(200) // Brief yellow flash
|
||||||
|
|
||||||
|
// Phase 3: Reveal final character
|
||||||
|
val text = currentText()
|
||||||
|
if (index < text.length) {
|
||||||
|
val newText = text.toCharArray()
|
||||||
|
newText[index] = targetChar
|
||||||
|
updateText(String(newText))
|
||||||
|
|
||||||
|
val finalStates = currentText().indices.map { i ->
|
||||||
|
when {
|
||||||
|
i <= index -> CharacterAnimationState.FINAL
|
||||||
|
else -> CharacterAnimationState.ENCRYPTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateState(finalStates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a message should be animated based on its mining state
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun shouldAnimateMessage(messageId: String): Boolean {
|
||||||
|
val miningMessages by PoWMiningTracker.miningMessages.collectAsState()
|
||||||
|
return miningMessages.contains(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks which messages are currently being mined for PoW
|
||||||
|
* Provides reactive state for UI animations
|
||||||
|
*/
|
||||||
|
object PoWMiningTracker {
|
||||||
|
private val _miningMessages = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val miningMessages: StateFlow<Set<String>> = _miningMessages.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start tracking a message as mining
|
||||||
|
*/
|
||||||
|
fun startMiningMessage(messageId: String) {
|
||||||
|
_miningMessages.value = _miningMessages.value + messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop tracking a message as mining
|
||||||
|
*/
|
||||||
|
fun stopMiningMessage(messageId: String) {
|
||||||
|
_miningMessages.value = _miningMessages.value - messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a message is currently mining
|
||||||
|
*/
|
||||||
|
fun isMiningMessage(messageId: String): Boolean {
|
||||||
|
return _miningMessages.value.contains(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all mining messages (for cleanup)
|
||||||
|
*/
|
||||||
|
fun clearAllMining() {
|
||||||
|
_miningMessages.value = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced message display that shows matrix animation during PoW mining
|
||||||
|
* This version integrates with the full message display system
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MessageWithMatrixAnimation(
|
||||||
|
message: com.bitchat.android.model.BitchatMessage,
|
||||||
|
currentUserNickname: String,
|
||||||
|
meshService: com.bitchat.android.mesh.BluetoothMeshService,
|
||||||
|
colorScheme: androidx.compose.material3.ColorScheme,
|
||||||
|
timeFormatter: java.text.SimpleDateFormat,
|
||||||
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
|
onMessageLongPress: ((com.bitchat.android.model.BitchatMessage) -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
// For simplicity, just animate the entire message content
|
||||||
|
val baseAnnotatedText = formatMessageAsAnnotatedString(
|
||||||
|
message = message,
|
||||||
|
currentUserNickname = currentUserNickname,
|
||||||
|
meshService = meshService,
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
timeFormatter = timeFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use matrix animation only for the content
|
||||||
|
AnimatedMatrixText(
|
||||||
|
targetText = message.content,
|
||||||
|
isAnimating = shouldAnimateMessage(message.id),
|
||||||
|
color = colorScheme.onSurface,
|
||||||
|
fontSize = TextUnit.Unspecified,
|
||||||
|
fontWeight = if (message.sender == currentUserNickname || message.sender.startsWith("$currentUserNickname#")) {
|
||||||
|
FontWeight.Bold
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the start index of the message content (after timestamp and sender)
|
||||||
|
*/
|
||||||
|
private fun findContentStartIndex(messageText: String): Int {
|
||||||
|
// Look for pattern like "] <@sender> " to find where content starts
|
||||||
|
val pattern = """] <[^>]+> """.toRegex()
|
||||||
|
val match = pattern.find(messageText)
|
||||||
|
return match?.range?.last?.plus(1) ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the end index of the message content (before final timestamp)
|
||||||
|
*/
|
||||||
|
private fun findContentEndIndex(messageText: String): Int {
|
||||||
|
// Look for pattern like " [HH:mm:ss]" at the end
|
||||||
|
val pattern = """ \[\d{2}:\d{2}:\d{2}]$""".toRegex()
|
||||||
|
val match = pattern.find(messageText)
|
||||||
|
return match?.range?.first ?: messageText.length
|
||||||
|
}
|
||||||
@@ -166,101 +166,120 @@ private fun MessageTextWithClickableNicknames(
|
|||||||
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
onMessageLongPress: ((BitchatMessage) -> Unit)?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val annotatedText = formatMessageAsAnnotatedString(
|
// Check if this message should be animated during PoW mining
|
||||||
message = message,
|
val shouldAnimate = shouldAnimateMessage(message.id)
|
||||||
currentUserNickname = currentUserNickname,
|
|
||||||
meshService = meshService,
|
|
||||||
colorScheme = colorScheme,
|
|
||||||
timeFormatter = timeFormatter
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if this message was sent by self to avoid click interactions on own nickname
|
// If animation is needed, use the matrix animation component for content only
|
||||||
val isSelf = message.senderPeerID == meshService.myPeerID ||
|
if (shouldAnimate) {
|
||||||
message.sender == currentUserNickname ||
|
// Display message with matrix animation for content
|
||||||
message.sender.startsWith("$currentUserNickname#")
|
MessageWithMatrixAnimation(
|
||||||
|
message = message,
|
||||||
val haptic = LocalHapticFeedback.current
|
currentUserNickname = currentUserNickname,
|
||||||
val context = LocalContext.current
|
meshService = meshService,
|
||||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
colorScheme = colorScheme,
|
||||||
Text(
|
timeFormatter = timeFormatter,
|
||||||
text = annotatedText,
|
onNicknameClick = onNicknameClick,
|
||||||
modifier = modifier.pointerInput(message) {
|
onMessageLongPress = onMessageLongPress,
|
||||||
detectTapGestures(
|
modifier = modifier
|
||||||
onTap = { position ->
|
)
|
||||||
val layout = textLayoutResult ?: return@detectTapGestures
|
} else {
|
||||||
val offset = layout.getOffsetForPosition(position)
|
// Normal message display
|
||||||
// Nickname click only when not self
|
val annotatedText = formatMessageAsAnnotatedString(
|
||||||
if (!isSelf && onNicknameClick != null) {
|
message = message,
|
||||||
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
currentUserNickname = currentUserNickname,
|
||||||
tag = "nickname_click",
|
meshService = meshService,
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
timeFormatter = timeFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if this message was sent by self to avoid click interactions on own nickname
|
||||||
|
val isSelf = message.senderPeerID == meshService.myPeerID ||
|
||||||
|
message.sender == currentUserNickname ||
|
||||||
|
message.sender.startsWith("$currentUserNickname#")
|
||||||
|
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
Text(
|
||||||
|
text = annotatedText,
|
||||||
|
modifier = modifier.pointerInput(message) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { position ->
|
||||||
|
val layout = textLayoutResult ?: return@detectTapGestures
|
||||||
|
val offset = layout.getOffsetForPosition(position)
|
||||||
|
// Nickname click only when not self
|
||||||
|
if (!isSelf && onNicknameClick != null) {
|
||||||
|
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
||||||
|
tag = "nickname_click",
|
||||||
|
start = offset,
|
||||||
|
end = offset
|
||||||
|
)
|
||||||
|
if (nicknameAnnotations.isNotEmpty()) {
|
||||||
|
val nickname = nicknameAnnotations.first().item
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
onNicknameClick.invoke(nickname)
|
||||||
|
return@detectTapGestures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Geohash teleport (all messages)
|
||||||
|
val geohashAnnotations = annotatedText.getStringAnnotations(
|
||||||
|
tag = "geohash_click",
|
||||||
start = offset,
|
start = offset,
|
||||||
end = offset
|
end = offset
|
||||||
)
|
)
|
||||||
if (nicknameAnnotations.isNotEmpty()) {
|
if (geohashAnnotations.isNotEmpty()) {
|
||||||
val nickname = nicknameAnnotations.first().item
|
val geohash = geohashAnnotations.first().item
|
||||||
|
try {
|
||||||
|
val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
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)
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
onNicknameClick.invoke(nickname)
|
|
||||||
return@detectTapGestures
|
return@detectTapGestures
|
||||||
}
|
}
|
||||||
|
// URL open (all messages)
|
||||||
|
val urlAnnotations = annotatedText.getStringAnnotations(
|
||||||
|
tag = "url_click",
|
||||||
|
start = offset,
|
||||||
|
end = offset
|
||||||
|
)
|
||||||
|
if (urlAnnotations.isNotEmpty()) {
|
||||||
|
val raw = urlAnnotations.first().item
|
||||||
|
val resolved = if (raw.startsWith("http://", ignoreCase = true) || raw.startsWith("https://", ignoreCase = true)) raw else "https://$raw"
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(resolved))
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
return@detectTapGestures
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onMessageLongPress?.invoke(message)
|
||||||
}
|
}
|
||||||
// Geohash teleport (all messages)
|
)
|
||||||
val geohashAnnotations = annotatedText.getStringAnnotations(
|
},
|
||||||
tag = "geohash_click",
|
fontFamily = FontFamily.Monospace,
|
||||||
start = offset,
|
softWrap = true,
|
||||||
end = offset
|
overflow = TextOverflow.Visible,
|
||||||
)
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
if (geohashAnnotations.isNotEmpty()) {
|
color = colorScheme.onSurface
|
||||||
val geohash = geohashAnnotations.first().item
|
),
|
||||||
try {
|
onTextLayout = { result -> textLayoutResult = result }
|
||||||
val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(
|
)
|
||||||
context
|
}
|
||||||
)
|
|
||||||
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)
|
|
||||||
return@detectTapGestures
|
|
||||||
}
|
|
||||||
// URL open (all messages)
|
|
||||||
val urlAnnotations = annotatedText.getStringAnnotations(
|
|
||||||
tag = "url_click",
|
|
||||||
start = offset,
|
|
||||||
end = offset
|
|
||||||
)
|
|
||||||
if (urlAnnotations.isNotEmpty()) {
|
|
||||||
val raw = urlAnnotations.first().item
|
|
||||||
val resolved = if (raw.startsWith("http://", ignoreCase = true) || raw.startsWith("https://", ignoreCase = true)) raw else "https://$raw"
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(resolved))
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
context.startActivity(intent)
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
||||||
return@detectTapGestures
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress = {
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
onMessageLongPress?.invoke(message)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
softWrap = true,
|
|
||||||
overflow = TextOverflow.Visible,
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = colorScheme.onSurface
|
|
||||||
),
|
|
||||||
onTextLayout = { result -> textLayoutResult = result }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user