Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
28 changes: 28 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"permissions": {
"allow": [
"Bash(dir)",
"Bash(git clone https://github.com/meshtastic/Meshtastic-Android.git .)",
"Bash(git submodule update --init --recursive)",
"Bash(where sdkmanager)",
"Bash(echo $ANDROID_HOME)",
"Bash(./gradlew --version)",
"Bash(./gradlew assembleDebug)",
"Bash(where java)",
"Bash(\"C:\\Program Files\\Android\\Android Studio\\jbr\\bin\\java.exe\" -version)",
"Bash($env:JAVA_HOME=\"C:\\Program Files\\Android\\Android Studio\\jbr\")",
"Bash(export JAVA_HOME=\"C:/Program Files/Android/Android Studio/jbr\")",
"Read(//c/Users/**)",
"Bash(./gradlew :feature:messaging:assembleDebug)",
"Bash(./gradlew assembleFdroidDebug)",
"Bash(./gradlew assembleGoogleDebug)",
"Bash(./gradlew clean)",
"Bash(./gradlew :app:assembleFdroidDebug)",
"Bash(./gradlew clean assembleFdroidDebug)",
"Bash(./gradlew :app:assembleFdroidDebug --rerun-tasks)",
"Bash(./gradlew :app:assembleGoogleDebug)"
],
"deny": [],
"ask": []
}
}
Binary file added appid-change-error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion feature/messaging/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
<ID>LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )</ID>
<ID>LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, private val quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )</ID>
<ID>ModifierMissing:Message.kt$MessageScreen</ID>
<ID>ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)</ID>
<ID>MutableStateParam:MessageList.kt$selectedIds</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ fun MessageScreen(
handleQuickChatAction(
action = action,
messageInputState = messageInputState,
userLatitude = ourNode?.takeIf { it.validPosition != null }?.latitude,
userLongitude = ourNode?.takeIf { it.validPosition != null }?.longitude,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this not a named tuple?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it is.

onSendMessage = { text -> onEvent(MessageScreenEvent.SendMessage(text)) },
)
},
Expand Down Expand Up @@ -422,25 +424,37 @@ private fun String.ellipsize(maxLength: Int): String = if (length > maxLength) "
*
* @param action The [QuickChatAction] to handle.
* @param messageInputState The [TextFieldState] of the message input field.
* @param userLatitude Current user latitude, if available.
* @param userLongitude Current user longitude, if available.
* @param onSendMessage Lambda to call when a message needs to be sent.
*/
private fun handleQuickChatAction(
action: QuickChatAction,
messageInputState: TextFieldState,
userLatitude: Double?,
userLongitude: Double?,
onSendMessage: (String) -> Unit,
) {
val processedMessage =
if (userLatitude != null && userLongitude != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be checking for the presence of the string before it does any other logic.

if message.lower.contains ('%gps') 
     if <valid gps data> 
         <do the replace> 

if <any other quick message params we want?> 




Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Now it does.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and now it does, properly.

val gpsString = "%.7f,%.7f".format(userLatitude, userLongitude)
action.message.replace("%GPS", gpsString, ignoreCase = true)
} else {
action.message
}

when (action.mode) {
QuickChatAction.Mode.Append -> {
val originalText = messageInputState.text.toString()
// Avoid appending if the exact message is already present (simple check)
if (!originalText.contains(action.message)) {
if (!originalText.contains(processedMessage)) {
val newText =
buildString {
append(originalText)
if (originalText.isNotEmpty() && !originalText.endsWith(' ')) {
append(' ')
}
append(action.message)
append(processedMessage)
}
.limitBytes(MESSAGE_CHARACTER_LIMIT_BYTES)
messageInputState.setTextAndPlaceCursorAtEnd(newText)
Expand All @@ -449,7 +463,7 @@ private fun handleQuickChatAction(

QuickChatAction.Mode.Instant -> {
// Byte limit for 'Send' mode messages is handled by the backend/transport layer.
onSendMessage(action.message)
onSendMessage(processedMessage)
}
}
}
Expand Down Expand Up @@ -707,7 +721,7 @@ private fun QuickChatRow(
val allActions = remember(alertAction, actions) { listOf(alertAction) + actions }

LazyRow(modifier = modifier.padding(vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
items(allActions, key = { it.uuid }) { action ->
items(allActions, key = { it.position }) { action ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we changing this key?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key was changed from uuid to position to fix a crash: Key "0" was already used.

Both the alert bell action and the default location pin action are created with uuid=0L (the default
value). When both are present in the LazyRow, this causes a key collision that crashes the app.

The position field is guaranteed unique:

Bell action: position = -1
Location pin action: position = -2 (or could be 0 if it's user-editable)
User-created actions: position = 0, 1, 2...
This ensures each item in the LazyRow has a unique, stable key for proper Compose recomposition.

Button(onClick = { onClick(action) }, enabled = enabled) { Text(text = action.name) }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class MessageViewModel
constructor(
private val nodeRepository: NodeRepository,
radioConfigRepository: RadioConfigRepository,
quickChatActionRepository: QuickChatActionRepository,
private val quickChatActionRepository: QuickChatActionRepository,
private val serviceRepository: ServiceRepository,
private val packetRepository: PacketRepository,
private val uiPrefs: UiPrefs,
Expand All @@ -77,6 +77,26 @@ constructor(

val quickChatActions = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList())

init {
viewModelScope.launch(Dispatchers.IO) {
val actions = quickChatActionRepository.getAllActions()
var isEmpty = true
actions.collect { list ->
if (isEmpty && list.isEmpty()) {
quickChatActionRepository.upsert(
org.meshtastic.core.database.entity.QuickChatAction(
name = "📍",
message = "https://maps.google.com/?q=%GPS",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not going to serve a google link to the mesh by default.

Copy link
Author

@suteny0r suteny0r Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll make it conditional on whether it's the f-droid or google build to use google maps or openstreetmaps.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layout of the OSM URL required that I separate the %GPS variable into %LAT and %LON. The app now constructs the appropriate URL, depending on whether it is the fdroid or google build.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have a geo intent (geo:47.62148,-122.34814), which the app will highlight. When clicked android opens a list of apps which handle geolocation, and a user can also set a default app.

Geo urls can also be expanded to include other metadata sent to the receiving app, like a label to put on the point, e.g. geo:47.62148,-122.34814(FriendXYZ @ 2025-11-09 16:40).

This format still allows the lat/long to be copied/pasted manually.

Code to send the intent is something like: (note AI assisted example)

fun openLabeledGeoIntent(latitude: Double, longitude: Double, label: String? = null) {
    val uriString = if (!label.isNullOrEmpty()) {
        "geo:0,0?q=$latitude,$longitude(${Uri.encode(label)})" // Slightly different format than the sending format due to better app support if using a label
    } else {
        "geo:$latitude,$longitude"
    }
    val geoUri = Uri.parse(uriString)
    val mapIntent = Intent(Intent.ACTION_VIEW, geoUri)
    mapIntent.resolveActivity(packageManager)?.let {
        startActivity(mapIntent)
    }
}

Usage:

openLabeledGeoIntent(47.62148, -122.34814, "FriendXYZ @ 2025-11-09 16:40")

Copy link
Author

@suteny0r suteny0r Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have a geo intent (geo:47.62148,-122.34814), which the app will highlight. When clicked android opens a list of apps which handle geolocation, and a user can also set a default app.

Geo urls can also be expanded to include other metadata sent to the receiving app, like a label to put on the point, e.g. geo:47.62148,-122.34814(FriendXYZ @ 2025-11-09 16:40).

This format still allows the lat/long to be copied/pasted manually.

Code to send the intent is something like: (note AI assisted example)

fun openLabeledGeoIntent(latitude: Double, longitude: Double, label: String? = null) {
    val uriString = if (!label.isNullOrEmpty()) {
        "geo:0,0?q=$latitude,$longitude(${Uri.encode(label)})" // Slightly different format than the sending format due to better app support if using a label
    } else {
        "geo:$latitude,$longitude"
    }
    val geoUri = Uri.parse(uriString)
    val mapIntent = Intent(Intent.ACTION_VIEW, geoUri)
    mapIntent.resolveActivity(packageManager)?.let {
        startActivity(mapIntent)
    }
}

Usage:

openLabeledGeoIntent(47.62148, -122.34814, "FriendXYZ @ 2025-11-09 16:40")

Would this be a less portable way to implement the feature? There is an iOS client as well, which I hope to port this feature to, once the quickchat feature is implemented there. There is also a web client.

Remember, it's just a quickchat message template. It builds the template differently depending on whether the build is for f-droid (OSM) or play store (Google Maps), but the template can be edited to support any mapping service that lets you pass the coordinates as parameters.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar technique on the Meshtastic browser client:

Future item: I'd leave it for future fixes, but the linked to code could be further improved by adding a config item to allow the user to select which map service they'd like to use, as there isn't OS handling of geo intents which otherwise gives users control of which map app used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that the current browser client doesn't make even web urls clickable.

Since the current iOS client doesn't have quick chat yet, it can only view the links, not create them, and the web client can't even view the existing format without copy/paste, switching to this new method wouldn't be impacting the existing audience that much. I will see if I can get it working.

Thanks again for the input.

Copy link

@justinormont justinormont Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested your newer debug build (on an isolated dev phone); it's working well.

One outlier case, if GPS Mode = DISABLED is set on the node, the "%LAT,%LON" doesn't get replaced with coordinates, producing a sent message of "geo:%LAT,LON":

image

Though likely the same result, you could also test:

  • GPS Mode = NOT_PRESENT
  • Before the node's GPS has a GPS fix
  • GPS provided by the phone, which is under Settings => App => Provide phone location to mesh

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now, if quick chat message contains variables that require location, button is disabled unless location is available. PR updated. Builds updated.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works. Thanks.

When disabled, the 📍 quick chat looks like:
image

The user may not know why it's disabled; that said I don't see elsewhere in the app where small pop-ups are used to explain a UI element. LGTM.

mode = org.meshtastic.core.database.entity.QuickChatAction.Mode.Append,
position = 0,
),
)
}
isEmpty = false
}
}
}

private val contactKeyForMessages: MutableStateFlow<String?> = MutableStateFlow(null)
private val messagesForContactKey: StateFlow<List<Message>> =
contactKeyForMessages
Expand Down