Skip to content

Commit

Permalink
feat: Add replies to messages
Browse files Browse the repository at this point in the history
This commit adds the ability to reply to messages.
- Added a new Reply entity to the database.
- Added a new ReplyRow composable to display replies.
- Updated the MessageItem composable to display replies.
- Updated the MessageList composable to display replies.
- Updated the MeshService to handle replies.
- Updated the PacketRepository to handle replies.
- Updated the PacketDao to handle replies.
- Updated the MessageItem to allow reactions and replies to sent messages.
  • Loading branch information
jamesarich committed Dec 14, 2024
1 parent 6e24b0f commit 371d93b
Show file tree
Hide file tree
Showing 11 changed files with 825 additions and 70 deletions.
572 changes: 572 additions & 0 deletions app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/16.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.database.entity.ReactionEntity
import com.geeksville.mesh.database.entity.ReplyEntity

@Database(
entities = [
Expand All @@ -46,6 +47,7 @@ import com.geeksville.mesh.database.entity.ReactionEntity
MeshLog::class,
QuickChatAction::class,
ReactionEntity::class,
ReplyEntity::class,
],
autoMigrations = [
AutoMigration(from = 3, to = 4),
Expand All @@ -60,8 +62,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
AutoMigration(from = 13, to = 14),
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
],
version = 15,
version = 16,
exportSchema = true,
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.ReactionEntity
import com.geeksville.mesh.database.entity.ReplyEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -107,4 +108,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) {
packetDao.insert(reaction)
}

suspend fun insertReply(reply: ReplyEntity) = withContext(Dispatchers.IO) {
packetDao.insert(reply)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.PacketEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.ReactionEntity
import com.geeksville.mesh.database.entity.ReplyEntity
import kotlinx.coroutines.flow.Flow

@Dao
Expand Down Expand Up @@ -214,4 +215,7 @@ interface PacketDao {

@Upsert
suspend fun insert(reaction: ReactionEntity)

@Upsert
suspend fun insert(reply: ReplyEntity)
}
37 changes: 37 additions & 0 deletions app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ data class PacketEntity(
@Embedded val packet: Packet,
@Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
val reactions: List<ReactionEntity> = emptyList(),
@Relation(entity = ReplyEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
val replies: List<ReplyEntity> = emptyList(),
) {
suspend fun toMessage(getNode: suspend (userId: String?) -> NodeEntity) = with(packet) {
Message(
Expand All @@ -45,6 +47,7 @@ data class PacketEntity(
routingError = routingError,
packetId = packetId,
emojis = reactions.toReaction(getNode),
replies = replies.toReply(getNode),
)
}
}
Expand Down Expand Up @@ -112,3 +115,37 @@ private suspend fun ReactionEntity.toReaction(
private suspend fun List<ReactionEntity>.toReaction(
getNode: suspend (userId: String?) -> NodeEntity
) = this.map { it.toReaction(getNode) }

data class Reply(
val replyId: Int,
val user: User,
val message: String,
val timestamp: Long,
)

@Entity(
tableName = "replies",
primaryKeys = ["reply_id", "user_id", "message"],
indices = [
Index(value = ["reply_id"]),
],
)
data class ReplyEntity(
@ColumnInfo(name = "reply_id") val replyId: Int,
@ColumnInfo(name = "user_id") val userId: String,
val message: String,
val timestamp: Long,
)

private suspend fun ReplyEntity.toReply(
getNode: suspend (userId: String?) -> NodeEntity
) = Reply(
replyId = replyId,
user = getNode(userId).user,
message = message,
timestamp = timestamp,
)

private suspend fun List<ReplyEntity>.toReply(
getNode: suspend (userId: String?) -> NodeEntity
) = this.map { it.toReply(getNode) }
2 changes: 2 additions & 0 deletions app/src/main/java/com/geeksville/mesh/model/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Reaction
import com.geeksville.mesh.database.entity.Reply

val Routing.Error.stringRes: Int
get() = when (this) {
Expand Down Expand Up @@ -55,6 +56,7 @@ data class Message(
val routingError: Int,
val packetId: Int,
val emojis: List<Reaction>,
val replies: List<Reply>,
) {
private fun getStatusStringRes(value: Int): Int {
val error = Routing.Error.forNumber(value) ?: Routing.Error.UNRECOGNIZED
Expand Down
48 changes: 45 additions & 3 deletions app/src/main/java/com/geeksville/mesh/service/MeshService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,62 @@ import android.os.IBinder
import android.os.RemoteException
import androidx.core.app.ServiceCompat
import androidx.core.location.LocationCompat
import com.geeksville.mesh.*
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.Position
import com.geeksville.mesh.R
import com.geeksville.mesh.StoreAndForwardProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.TelemetryProtos.LocalStats
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.ReactionEntity
import com.geeksville.mesh.database.entity.ReplyEntity
import com.geeksville.mesh.database.entity.toNodeInfo
import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.getTracerouteResponse
import com.geeksville.mesh.position
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.network.MQTTRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.RadioServiceConnectionState
import com.geeksville.mesh.util.*
import com.geeksville.mesh.telemetry
import com.geeksville.mesh.user
import com.geeksville.mesh.util.anonymize
import com.geeksville.mesh.util.toOneLineString
import com.geeksville.mesh.util.toPIIString
import com.geeksville.mesh.util.toRemoteExceptions
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import dagger.Lazy
Expand Down Expand Up @@ -645,6 +674,16 @@ class MeshService : Service(), Logging {
packetRepository.get().insertReaction(reaction)
}

private fun rememberReply(packet: MeshPacket) = serviceScope.handledLaunch {
val reply = ReplyEntity(
replyId = packet.decoded.replyId,
userId = toNodeID(packet.from),
message = packet.decoded.payload.toByteArray().decodeToString(),
timestamp = System.currentTimeMillis(),
)
packetRepository.get().insertReply(reply)
}



private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) {
Expand Down Expand Up @@ -702,6 +741,9 @@ class MeshService : Service(), Logging {
if (data.emoji != 0) {
debug("Received EMOJI from $fromId")
rememberReaction(packet)
} else if (data.replyId != 0) {
debug("Received REPLY from $fromId")
rememberReply(packet)
} else {
debug("Received CLEAR_TEXT from $fromId")
rememberDataPacket(dataPacket)
Expand Down Expand Up @@ -1810,7 +1852,7 @@ class MeshService : Service(), Logging {
payload = ByteString.copyFrom(reply.message.encodeToByteArray())
}
sendToRadio(packet)
// rememberReply(packet.copy { from = myNodeNum })
rememberReply(packet.copy { from = myNodeNum })
}

private val binder = object : IMeshService.Stub() {
Expand Down
14 changes: 9 additions & 5 deletions app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.twotone.Reply
import androidx.compose.material.icons.automirrored.twotone.Send
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.SelectAll
Expand Down Expand Up @@ -290,9 +290,13 @@ internal fun MessageScreen(
selectedIds = selectedIds,
onUnreadChanged = { viewModel.clearUnreadCount(contactKey, it) },
contentPadding = innerPadding,
onSendReaction = { emoji, id -> viewModel.sendReaction(emoji, id, contactKey) },
onSendReaction = { emoji, id ->
viewModel.sendReaction(emoji, id, contactKey)
selectedIds.value = emptySet()
},
onReplyClick = { msg ->
replyingTo = msg
selectedIds.value = emptySet()
},
) { action ->
when (action) {
Expand Down Expand Up @@ -489,9 +493,9 @@ private fun TextInput(
) {
Icon(
imageVector = if (isReply) {
Icons.AutoMirrored.Filled.Reply
Icons.AutoMirrored.TwoTone.Reply
} else {
Icons.AutoMirrored.Default.Send
Icons.AutoMirrored.TwoTone.Send
},
contentDescription = stringResource(id = R.string.send_text),
modifier = Modifier.scale(scale = 1.5f),
Expand Down
Loading

0 comments on commit 371d93b

Please sign in to comment.