Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.services.AppStateStore
import com.bitchat.android.services.ContactDirectory
import com.bitchat.android.services.ConversationListPreferences
import com.bitchat.android.services.MessageRouter
import com.bitchat.android.ui.NotificationManager
import com.bitchat.android.util.TrackingUrlDetector
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -40,6 +42,20 @@ class ConversationNotificationReceiver : BroadcastReceiver() {
?.trim()
?.takeIf(String::isNotEmpty)
?: return@launch
if (TrackingUrlDetector.containsTrackingUrl(reply)) {
ConversationListPreferences.getInstance(context).appendDraft(
conversationID,
reply,
)
NotificationManager.showTrackingReplyWarning(
context = context.applicationContext,
conversationID = conversationID,
senderNickname = intent.getStringExtra(
NotificationManager.EXTRA_SENDER_NICKNAME,
).orEmpty(),
)
return@launch
}
// A notification can outlive the process/service that posted it. Promote
// the mesh runtime before dispatch so Android keeps the transport alive
// after this short-lived receiver finishes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ internal class ConversationListPreferences private constructor(
fun isPinned(conversationID: String): Boolean =
normalize(conversationID) in _pinned.value

@Synchronized
fun draftFor(conversationID: String): String? =
_drafts.value[normalize(conversationID)]

@Synchronized
fun setDraft(conversationID: String, text: String) {
val key = normalize(conversationID)
val updated = _drafts.value.toMutableMap()
Expand All @@ -88,6 +90,21 @@ internal class ConversationListPreferences private constructor(
saveDrafts(retained)
}

@Synchronized
fun appendDraft(conversationID: String, text: String) {
val key = normalize(conversationID)
val updated = _drafts.value.toMutableMap()
updated.remove(key)
updated[key] = mergeConversationDrafts(
existingDraft = _drafts.value[key],
appendedText = text,
maxChars = MAX_DRAFT_CHARS,
)
val retained = boundDrafts(updated)
_drafts.value = retained
saveDrafts(retained)
}

fun removeConversation(conversationID: String) {
val key = normalize(conversationID)
_pinned.value = _pinned.value - key
Expand Down Expand Up @@ -192,3 +209,18 @@ internal class ConversationListPreferences private constructor(
}

}

internal fun mergeConversationDrafts(
existingDraft: String?,
appendedText: String,
maxChars: Int,
): String {
val boundedAppend = appendedText.takeLast(maxChars)
val existing = existingDraft?.takeIf(String::isNotBlank) ?: return boundedAppend
val existingLimit = (maxChars - boundedAppend.length - 1).coerceAtLeast(0)
return if (existingLimit == 0) {
boundedAppend
} else {
existing.take(existingLimit) + "\n" + boundedAppend
}
}
31 changes: 26 additions & 5 deletions app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -47,6 +48,7 @@ import com.bitchat.android.nostr.LocationNotesManager
import com.bitchat.android.nostr.NearbyNotesController
import com.bitchat.android.ui.media.FullScreenImageViewer
import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.util.TrackingUrlDetector

/**
* Main ChatScreen - REFACTORED to use component-based architecture
Expand Down Expand Up @@ -395,9 +397,9 @@ fun ChatScreen(viewModel: ChatViewModel) {
viewModel.updateCommandSuggestions(newText.text)
viewModel.updateMentionSuggestions(newText.text)
},
onSend = {
if (messageText.text.trim().isNotEmpty()) {
viewModel.sendMessage(messageText.text.trim()) { accepted ->
onSend = { submittedText ->
if (submittedText.trim().isNotEmpty()) {
viewModel.sendMessage(submittedText.trim()) { accepted ->
if (accepted) {
messageText = TextFieldValue("")
viewModel.setConversationDraft(selectedPrivatePeer, "")
Expand Down Expand Up @@ -628,7 +630,7 @@ private fun NearbyNotesStrip(
fun ChatInputSection(
messageText: TextFieldValue,
onMessageTextChange: (TextFieldValue) -> Unit,
onSend: () -> Unit,
onSend: (String) -> Unit,
onSendVoiceNote: (String?, String?, String) -> Unit,
onSendImageNote: (String?, String?, String) -> Unit,
onSendFileNote: (String?, String?, String) -> Unit,
Expand All @@ -648,9 +650,17 @@ fun ChatInputSection(
modifier: Modifier = Modifier
) {
val context = androidx.compose.ui.platform.LocalContext.current
var pendingTrackedText by rememberSaveable { mutableStateOf<String?>(null) }
val activePublicTalker by remember(context) {
com.bitchat.android.features.voice.LiveVoiceManager.getInstance(context).activePublicTalker
}.collectAsState()
val requestSend = {
if (TrackingUrlDetector.containsTrackingUrl(messageText.text)) {
pendingTrackedText = messageText.text
} else {
onSend(messageText.text)
}
}
Column(
// Flat, slightly translucent screen background — the same treatment as the top bar, so the
// two bars are visibly the same kind of surface. No gradient: a soft ramp here just looked
Expand Down Expand Up @@ -718,7 +728,7 @@ fun ChatInputSection(
MessageInput(
value = messageText,
onValueChange = onMessageTextChange,
onSend = onSend,
onSend = requestSend,
onSendVoiceNote = onSendVoiceNote,
onSendImageNote = onSendImageNote,
onSendFileNote = onSendFileNote,
Expand All @@ -732,6 +742,17 @@ fun ChatInputSection(
modifier = Modifier.fillMaxWidth()
)
}

pendingTrackedText?.let { submittedText ->
TrackingWarningDialog(
message = R.string.tracking_link_send_warning,
onConfirm = {
pendingTrackedText = null
onSend(submittedText)
},
onDismiss = { pendingTrackedText = null },
)
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,8 @@ class ChatViewModel(
?.let(conversationListPreferences.drafts.value::get)
.orEmpty()

internal val conversationDrafts = conversationListPreferences.drafts

internal fun setConversationDraft(conversationID: String?, text: String) {
if (conversationID.isNullOrBlank()) return
conversationListPreferences.setDraft(conversationID, text)
Expand Down
25 changes: 23 additions & 2 deletions app/src/main/java/com/bitchat/android/ui/LocationNotesSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -32,6 +33,7 @@ import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.nostr.LocationNotesManager
import com.bitchat.android.nostr.NearbyNotesController
import com.bitchat.android.util.TrackingUrlDetector
import java.text.SimpleDateFormat
import java.util.*
import java.util.Calendar
Expand Down Expand Up @@ -76,6 +78,7 @@ fun LocationNotesSheet(

// Input field state
var draft by remember { mutableStateOf("") }
var pendingTrackedText by rememberSaveable { mutableStateOf<String?>(null) }
val sendButtonEnabled = draft.trim().isNotEmpty() && state != LocationNotesManager.State.NO_RELAYS

// Scroll state
Expand Down Expand Up @@ -216,15 +219,33 @@ fun LocationNotesSheet(
onSend = {
val content = draft.trim()
if (content.isNotEmpty()) {
notesManager.send(content, nickname)
draft = ""
if (TrackingUrlDetector.containsTrackingUrl(content)) {
pendingTrackedText = content
} else {
notesManager.send(content, nickname)
draft = ""
}
}
}
)
}
}
}
}

pendingTrackedText?.let { content ->
TrackingWarningDialog(
message = R.string.tracking_link_send_warning,
onConfirm = {
pendingTrackedText = null
if (content.isNotEmpty()) {
notesManager.send(content, nickname)
draft = ""
}
},
onDismiss = { pendingTrackedText = null },
)
}
}

/**
Expand Down
21 changes: 17 additions & 4 deletions app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1836,13 +1836,26 @@ fun PrivateChatSheet(

// Input section. No divider here: ChatInputSection draws its own fade and
// hairline.
val conversationDrafts by viewModel.conversationDrafts
.collectAsStateWithLifecycle()
var messageText by remember(peerID) {
mutableStateOf(
androidx.compose.ui.text.input.TextFieldValue(
viewModel.conversationDraft(peerID)
)
)
}
val persistedDraft = conversationDrafts[
ContactDirectory.canonicalConversationId(peerID).lowercase()
].orEmpty()
LaunchedEffect(persistedDraft) {
if (persistedDraft != messageText.text) {
messageText = androidx.compose.ui.text.input.TextFieldValue(
text = persistedDraft,
selection = androidx.compose.ui.text.TextRange(persistedDraft.length),
)
}
}

ChatInputSection(
messageText = messageText,
Expand All @@ -1853,10 +1866,10 @@ fun PrivateChatSheet(
// renders its own popups as hidden, so an update only leaves
// a stale popup behind for the main composer.
},
onSend = {
if (messageText.text.trim().isNotEmpty()) {
viewModel.sendMessage(messageText.text.trim()) { accepted ->
if (accepted) {
onSend = { submittedText ->
if (submittedText.trim().isNotEmpty()) {
viewModel.sendMessage(submittedText.trim()) { accepted ->
if (accepted && messageText.text == submittedText) {
messageText =
androidx.compose.ui.text.input.TextFieldValue("")
viewModel.setConversationDraft(peerID, "")
Expand Down
34 changes: 26 additions & 8 deletions app/src/main/java/com/bitchat/android/ui/MessageComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.ui.media.FileMessageItem
import com.bitchat.android.util.TrackingUrlDetector
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.ui.theme.ChatUiModeManager
Expand Down Expand Up @@ -761,6 +762,15 @@ internal fun TextMessageLayout(
val isSelf = message.isFromSelf(currentUserNickname, myPeerId)
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
var pendingTrackedUrl by rememberSaveable { mutableStateOf<String?>(null) }
val openOrWarn: (String) -> Unit = { rawUrl ->
if (TrackingUrlDetector.hasTrackingParameter(normalizeMessageUrl(rawUrl))) {
pendingTrackedUrl = rawUrl
} else {
openMessageUrl(context, rawUrl)
}
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
val handleLongPress: () -> Unit = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onMessageLongPress?.invoke(message)
Expand Down Expand Up @@ -802,12 +812,10 @@ internal fun TextMessageLayout(
timeFormatter = timeFormatter,
onNicknameClick = onNicknameClick,
onLongPress = handleLongPress,
onUrlClick = openOrWarn,
modifier = modifier,
)
return
}

Column(
} else Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MessageGrouping.SENDER_TO_BODY_SPACING),
) {
Expand Down Expand Up @@ -847,8 +855,7 @@ internal fun TextMessageLayout(
}

"url_click" -> {
openMessageUrl(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
openOrWarn(item)
true
}

Expand All @@ -862,6 +869,17 @@ internal fun TextMessageLayout(
style = MessageBodyTextStyle.copy(color = colorScheme.onSurface),
)
}

pendingTrackedUrl?.let { url ->
TrackingWarningDialog(
message = R.string.tracking_link_open_warning,
onConfirm = {
pendingTrackedUrl = null
openMessageUrl(context, url)
},
onDismiss = { pendingTrackedUrl = null },
)
}
}

/**
Expand All @@ -884,6 +902,7 @@ private fun BubbleTextMessageLayout(
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
onLongPress: () -> Unit,
onUrlClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val palette = LocalBitchatPalette.current
Expand Down Expand Up @@ -1002,8 +1021,7 @@ private fun BubbleTextMessageLayout(
}

"url_click" -> {
openMessageUrl(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onUrlClick(item)
true
}

Expand Down
Loading
Loading