mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Send emoji codepoint in reaction packets (#4123)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
5b1693aa04
commit
c9259c793f
6 changed files with 85 additions and 56 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
|
@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.callbackFlow
|
|||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -89,30 +89,37 @@ private fun NsdManager.discoverServices(
|
|||
@SuppressLint("NewApi")
|
||||
private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdServiceInfo? =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val isResumed = AtomicBoolean(false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val callback =
|
||||
object : NsdManager.ServiceInfoCallback {
|
||||
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||
continuation.resume(null)
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceUpdated(updatedServiceInfo: NsdServiceInfo) {
|
||||
if (updatedServiceInfo.hostAddresses.isNotEmpty()) {
|
||||
continuation.resume(updatedServiceInfo)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(updatedServiceInfo)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceLost() {
|
||||
continuation.resume(null)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
try {
|
||||
unregisterServiceInfoCallback(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.w(e) { "Already unregistered" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +139,15 @@ private suspend fun NsdManager.resolveService(serviceInfo: NsdServiceInfo): NsdS
|
|||
val listener =
|
||||
object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
continuation.resume(null)
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
continuation.resume(serviceInfo)
|
||||
if (isResumed.compareAndSet(false, true)) {
|
||||
continuation.resume(serviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
|
|
|
|||
|
|
@ -71,44 +71,10 @@ constructor(
|
|||
ignoreException {
|
||||
val myNodeNum = nodeManager.myNodeNum ?: return@ignoreException
|
||||
when (action) {
|
||||
is ServiceAction.Favorite -> {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isFavorite) removeFavoriteNode = node.num else setFavoriteNode = node.num
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
|
||||
}
|
||||
is ServiceAction.Ignore -> {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isIgnored) removeIgnoredNode = node.num else setIgnoredNode = node.num
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
||||
}
|
||||
is ServiceAction.Reaction -> {
|
||||
val channel = action.contactKey[0].digitToInt()
|
||||
val destId = action.contactKey.substring(1)
|
||||
val dataPacket =
|
||||
org.meshtastic.core.model.DataPacket(
|
||||
to = destId,
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = action.emoji.encodeToByteArray(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = false,
|
||||
)
|
||||
commandSender.sendData(dataPacket)
|
||||
rememberReaction(action)
|
||||
}
|
||||
is ServiceAction.ImportContact -> {
|
||||
val verifiedContact = action.contact.toBuilder().setManuallyVerified(true).build()
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = verifiedContact }
|
||||
nodeManager.handleReceivedUser(
|
||||
verifiedContact.nodeNum,
|
||||
verifiedContact.user,
|
||||
manuallyVerified = true,
|
||||
)
|
||||
}
|
||||
is ServiceAction.Favorite -> handleFavorite(action, myNodeNum)
|
||||
is ServiceAction.Ignore -> handleIgnore(action, myNodeNum)
|
||||
is ServiceAction.Reaction -> handleReaction(action)
|
||||
is ServiceAction.ImportContact -> handleImportContact(action, myNodeNum)
|
||||
is ServiceAction.SendContact -> {
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = action.contact }
|
||||
}
|
||||
|
|
@ -119,6 +85,45 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFavorite(action: ServiceAction.Favorite, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isFavorite) removeFavoriteNode = node.num else setFavoriteNode = node.num
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
|
||||
}
|
||||
|
||||
private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isIgnored) removeIgnoredNode = node.num else setIgnoredNode = node.num
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
||||
}
|
||||
|
||||
private fun handleReaction(action: ServiceAction.Reaction) {
|
||||
val channel = action.contactKey[0].digitToInt()
|
||||
val destId = action.contactKey.substring(1)
|
||||
val dataPacket =
|
||||
org.meshtastic.core.model.DataPacket(
|
||||
to = destId,
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = action.emoji.encodeToByteArray(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = false,
|
||||
emoji = action.emoji.codePointAt(0),
|
||||
)
|
||||
commandSender.sendData(dataPacket)
|
||||
rememberReaction(action)
|
||||
}
|
||||
|
||||
private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
|
||||
val verifiedContact = action.contact.toBuilder().setManuallyVerified(true).build()
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = verifiedContact }
|
||||
nodeManager.handleReceivedUser(verifiedContact.nodeNum, verifiedContact.user, manuallyVerified = true)
|
||||
}
|
||||
|
||||
private fun rememberReaction(action: ServiceAction.Reaction) {
|
||||
scope.handledLaunch {
|
||||
val reaction =
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ constructor(
|
|||
portnumValue = p.dataType
|
||||
payload = ByteString.copyFrom(p.bytes ?: ByteArray(0))
|
||||
p.replyId?.let { if (it != 0) replyId = it }
|
||||
if (p.emoji != 0) emoji = p.emoji
|
||||
}
|
||||
p.time = System.currentTimeMillis()
|
||||
packetHandler?.sendToRadio(meshPacket)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManage
|
|||
replyId = data.replyId,
|
||||
relayNode = packet.relayNode,
|
||||
viaMqtt = packet.viaMqtt,
|
||||
emoji = data.emoji,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import android.os.Parcel
|
||||
|
|
@ -64,6 +63,7 @@ data class DataPacket(
|
|||
var relayNode: Int? = null,
|
||||
var relays: Int = 0,
|
||||
var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path
|
||||
var emoji: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
/** If there was an error with this message, this string describes what was wrong. */
|
||||
|
|
@ -137,6 +137,9 @@ data class DataPacket(
|
|||
parcel.readInt(),
|
||||
parcel.readInt().let { if (it == 0) null else it },
|
||||
parcel.readInt().let { if (it == -1) null else it },
|
||||
parcel.readInt(), // relays
|
||||
parcel.readInt() == 1, // viaMqtt
|
||||
parcel.readInt(), // emoji
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
@ -161,6 +164,7 @@ data class DataPacket(
|
|||
if (rssi != other.rssi) return false
|
||||
if (replyId != other.replyId) return false
|
||||
if (relayNode != other.relayNode) return false
|
||||
if (emoji != other.emoji) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -181,6 +185,7 @@ data class DataPacket(
|
|||
result = 31 * result + rssi
|
||||
result = 31 * result + replyId.hashCode()
|
||||
result = 31 * result + relayNode.hashCode()
|
||||
result = 31 * result + emoji
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -200,6 +205,9 @@ data class DataPacket(
|
|||
parcel.writeInt(rssi)
|
||||
parcel.writeInt(replyId ?: 0)
|
||||
parcel.writeInt(relayNode ?: -1)
|
||||
parcel.writeInt(relays)
|
||||
parcel.writeInt(if (viaMqtt) 1 else 0)
|
||||
parcel.writeInt(emoji)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
|
@ -221,6 +229,9 @@ data class DataPacket(
|
|||
rssi = parcel.readInt()
|
||||
replyId = parcel.readInt().let { if (it == 0) null else it }
|
||||
relayNode = parcel.readInt().let { if (it == -1) null else it }
|
||||
relays = parcel.readInt()
|
||||
viaMqtt = parcel.readInt() == 1
|
||||
emoji = parcel.readInt()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ org.gradle.configuration-cache=true
|
|||
|
||||
# Watches the file system for changes, allowing Gradle to reuse information about the file system
|
||||
# between builds.
|
||||
org.gradle.vfs.watch=true
|
||||
# org.gradle.vfs.watch=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue