diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/14.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/14.json new file mode 100644 index 000000000..93b3614dd --- /dev/null +++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/14.json @@ -0,0 +1,515 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "b610881191518f933ee6bb694d12d0a2", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `reply_id` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b610881191518f933ee6bb694d12d0a2')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt b/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt index be9b58beb..4141fe007 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt @@ -84,8 +84,8 @@ class PacketDaoTest { } packetDao = database.packetDao().apply { - generateTestPackets(42424243).forEach(::insert) - generateTestPackets(myNodeNum).forEach(::insert) + generateTestPackets(42424243).forEach { insert(it) } + generateTestPackets(myNodeNum).forEach { insert(it) } } } @@ -132,10 +132,10 @@ class PacketDaoTest { val messages = packetDao.getMessagesFrom(contactKey).first() assertEquals(SAMPLE_SIZE, messages.size) - val onlyFromContactKey = messages.all { it.contact_key == contactKey } + val onlyFromContactKey = messages.all { it.packet.contact_key == contactKey } assertTrue(onlyFromContactKey) - val onlyMyNodeNum = messages.all { it.myNodeNum == myNodeNum } + val onlyMyNodeNum = messages.all { it.packet.myNodeNum == myNodeNum } assertTrue(onlyMyNodeNum) } } diff --git a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt index 4c160ee2d..03da56143 100644 --- a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt +++ b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt @@ -1,88 +1,91 @@ -/* - * Copyright (c) 2024 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 - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.geeksville.mesh.database - -import android.content.Context -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.DeleteTable -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.room.migration.AutoMigrationSpec -import com.geeksville.mesh.database.dao.PacketDao -import com.geeksville.mesh.database.dao.MeshLogDao -import com.geeksville.mesh.database.dao.NodeInfoDao -import com.geeksville.mesh.database.dao.QuickChatActionDao -import com.geeksville.mesh.database.entity.ContactSettings -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.QuickChatAction - -@Database( - entities = [ - MyNodeEntity::class, - NodeEntity::class, - Packet::class, - ContactSettings::class, - MeshLog::class, - QuickChatAction::class - ], - autoMigrations = [ - AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5), - AutoMigration(from = 5, to = 6), - AutoMigration(from = 6, to = 7), - AutoMigration(from = 7, to = 8), - AutoMigration(from = 8, to = 9), - AutoMigration(from = 9, to = 10), - AutoMigration(from = 10, to = 11), - AutoMigration(from = 11, to = 12), - AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class), - ], - version = 13, - exportSchema = true, -) -@TypeConverters(Converters::class) -abstract class MeshtasticDatabase : RoomDatabase() { - abstract fun nodeInfoDao(): NodeInfoDao - abstract fun packetDao(): PacketDao - abstract fun meshLogDao(): MeshLogDao - abstract fun quickChatActionDao(): QuickChatActionDao - - companion object { - fun getDatabase(context: Context): MeshtasticDatabase { - - return Room.databaseBuilder( - context.applicationContext, - MeshtasticDatabase::class.java, - "meshtastic_database" - ) - .fallbackToDestructiveMigration() - .build() - } - } -} - -@DeleteTable.Entries( - DeleteTable(tableName = "NodeInfo"), - DeleteTable(tableName = "MyNodeInfo") -) -class AutoMigration12to13 : AutoMigrationSpec +/* + * Copyright (c) 2024 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.database + +import android.content.Context +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.DeleteTable +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.AutoMigrationSpec +import com.geeksville.mesh.database.dao.PacketDao +import com.geeksville.mesh.database.dao.MeshLogDao +import com.geeksville.mesh.database.dao.NodeInfoDao +import com.geeksville.mesh.database.dao.QuickChatActionDao +import com.geeksville.mesh.database.entity.ContactSettings +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.QuickChatAction +import com.geeksville.mesh.database.entity.ReactionEntity + +@Database( + entities = [ + MyNodeEntity::class, + NodeEntity::class, + Packet::class, + ContactSettings::class, + MeshLog::class, + QuickChatAction::class, + ReactionEntity::class, + ], + autoMigrations = [ + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7), + AutoMigration(from = 7, to = 8), + AutoMigration(from = 8, to = 9), + AutoMigration(from = 9, to = 10), + AutoMigration(from = 10, to = 11), + AutoMigration(from = 11, to = 12), + AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class), + AutoMigration(from = 13, to = 14), + ], + version = 14, + exportSchema = true, +) +@TypeConverters(Converters::class) +abstract class MeshtasticDatabase : RoomDatabase() { + abstract fun nodeInfoDao(): NodeInfoDao + abstract fun packetDao(): PacketDao + abstract fun meshLogDao(): MeshLogDao + abstract fun quickChatActionDao(): QuickChatActionDao + + companion object { + fun getDatabase(context: Context): MeshtasticDatabase { + + return Room.databaseBuilder( + context.applicationContext, + MeshtasticDatabase::class.java, + "meshtastic_database" + ) + .fallbackToDestructiveMigration() + .build() + } + } +} + +@DeleteTable.Entries( + DeleteTable(tableName = "NodeInfo"), + DeleteTable(tableName = "MyNodeInfo") +) +class AutoMigration12to13 : AutoMigrationSpec diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt index f63ba3b5b..c362b66db 100644 --- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt @@ -23,6 +23,7 @@ import com.geeksville.mesh.Portnums.PortNum 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -102,4 +103,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz suspend fun setMuteUntil(contacts: List, until: Long) = withContext(Dispatchers.IO) { packetDao.setMuteUntil(contacts, until) } + + suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) { + packetDao.insert(reaction) + } } diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt index 9e41fb0fe..ea7e59595 100644 --- a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt +++ b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt @@ -18,7 +18,6 @@ package com.geeksville.mesh.database.dao import androidx.room.Dao -import androidx.room.Insert import androidx.room.MapColumn import androidx.room.Update import androidx.room.Query @@ -27,7 +26,9 @@ import androidx.room.Upsert import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MessageStatus 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 kotlinx.coroutines.flow.Flow @Dao @@ -81,8 +82,8 @@ interface PacketDao { ) suspend fun clearUnreadCount(contact: String, timestamp: Long) - @Insert - fun insert(packet: Packet) + @Upsert + suspend fun insert(packet: Packet) @Query( """ @@ -92,7 +93,8 @@ interface PacketDao { ORDER BY received_time DESC """ ) - fun getMessagesFrom(contact: String): Flow> + @Transaction + fun getMessagesFrom(contact: String): Flow> @Query( """ @@ -101,10 +103,10 @@ interface PacketDao { AND data = :data """ ) - fun findDataPacket(data: DataPacket): Packet? + suspend fun findDataPacket(data: DataPacket): Packet? @Query("DELETE FROM packet WHERE uuid in (:uuidList)") - fun deleteMessages(uuidList: List) + suspend fun deletePackets(uuidList: List) @Query( """ @@ -113,27 +115,42 @@ interface PacketDao { AND contact_key IN (:contactList) """ ) - fun deleteContacts(contactList: List) + suspend fun deleteContacts(contactList: List) @Query("DELETE FROM packet WHERE uuid=:uuid") - fun _delete(uuid: Long) + suspend fun _delete(uuid: Long) @Transaction - fun delete(packet: Packet) { + suspend fun delete(packet: Packet) { _delete(packet.uuid) } + @Query("SELECT packet_id FROM packet WHERE uuid IN (:uuidList)") + suspend fun getPacketIdsFrom(uuidList: List): List + + @Query("DELETE FROM reactions WHERE reply_id IN (:packetIds)") + suspend fun deleteReactions(packetIds: List) + + @Transaction + suspend fun deleteMessages(uuidList: List) { + val packetIds = getPacketIdsFrom(uuidList) + if (packetIds.isNotEmpty()) { + deleteReactions(packetIds) + } + deletePackets(uuidList) + } + @Update - fun update(packet: Packet) + suspend fun update(packet: Packet) @Transaction - fun updateMessageStatus(data: DataPacket, m: MessageStatus) { + suspend fun updateMessageStatus(data: DataPacket, m: MessageStatus) { val new = data.copy(status = m) findDataPacket(data)?.let { update(it.copy(data = new)) } } @Transaction - fun updateMessageId(data: DataPacket, id: Int) { + suspend fun updateMessageId(data: DataPacket, id: Int) { val new = data.copy(id = id) findDataPacket(data)?.let { update(it.copy(data = new)) } } @@ -145,7 +162,7 @@ interface PacketDao { ORDER BY received_time ASC """ ) - fun getDataPackets(): List + suspend fun getDataPackets(): List @Query( """ @@ -155,10 +172,10 @@ interface PacketDao { ORDER BY received_time DESC """ ) - fun getPacketById(requestId: Int): Packet? + suspend fun getPacketById(requestId: Int): Packet? @Transaction - fun getQueuedPackets(): List? = + suspend fun getQueuedPackets(): List? = getDataPackets().filter { it.status == MessageStatus.QUEUED } @Query( @@ -169,10 +186,10 @@ interface PacketDao { ORDER BY received_time ASC """ ) - fun getAllWaypoints(): List + suspend fun getAllWaypoints(): List @Transaction - fun deleteWaypoint(id: Int) { + suspend fun deleteWaypoint(id: Int) { val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid } deleteMessages(uuidList) } @@ -184,7 +201,7 @@ interface PacketDao { suspend fun getContactSettings(contact: String): ContactSettings? @Upsert - fun upsertContactSettings(contacts: List) + suspend fun upsertContactSettings(contacts: List) @Transaction suspend fun setMuteUntil(contacts: List, until: Long) { @@ -194,4 +211,7 @@ interface PacketDao { } upsertContactSettings(contactList) } + + @Upsert + suspend fun insert(reaction: ReactionEntity) } diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt index 4045ebf1c..3634826c0 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt @@ -18,10 +18,36 @@ package com.geeksville.mesh.database.entity import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import androidx.room.Relation import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MeshProtos.User +import com.geeksville.mesh.model.Message +import com.geeksville.mesh.util.getShortDateTime + +data class PacketEntity( + @Embedded val packet: Packet, + @Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id") + val reactions: List = emptyList(), +) { + suspend fun toMessage(getUser: suspend (userId: String?) -> User) = with(packet) { + Message( + uuid = uuid, + receivedTime = received_time, + user = getUser(data.from), + text = data.text.orEmpty(), + time = getShortDateTime(data.time), + read = read, + status = data.status, + routingError = routingError, + packetId = packetId, + emojis = reactions.toReaction(getUser), + ) + } +} @Entity( tableName = "packet", @@ -42,6 +68,7 @@ data class Packet( @ColumnInfo(name = "data") val data: DataPacket, @ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0, @ColumnInfo(name = "routing_error", defaultValue = "-1") var routingError: Int = -1, + @ColumnInfo(name = "reply_id", defaultValue = "0") val replyId: Int = 0, ) @Entity(tableName = "contact_settings") @@ -51,3 +78,37 @@ data class ContactSettings( ) { val isMuted get() = System.currentTimeMillis() <= muteUntil } + +data class Reaction( + val replyId: Int, + val user: User, + val emoji: String, + val timestamp: Long, +) + +@Entity( + tableName = "reactions", + primaryKeys = ["reply_id", "user_id", "emoji"], + indices = [ + Index(value = ["reply_id"]), + ], +) +data class ReactionEntity( + @ColumnInfo(name = "reply_id") val replyId: Int, + @ColumnInfo(name = "user_id") val userId: String, + val emoji: String, + val timestamp: Long, +) + +private suspend fun ReactionEntity.toReaction( + getUser: suspend (userId: String?) -> User +) = Reaction( + replyId = replyId, + user = getUser(userId), + emoji = emoji, + timestamp = timestamp, +) + +private suspend fun List.toReaction( + getUser: suspend (userId: String?) -> User +) = this.map { it.toReaction(getUser) } diff --git a/app/src/main/java/com/geeksville/mesh/model/Message.kt b/app/src/main/java/com/geeksville/mesh/model/Message.kt index df8b568fa..61ef926fc 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Message.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Message.kt @@ -17,10 +17,11 @@ package com.geeksville.mesh.model -import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshProtos.Routing +import com.geeksville.mesh.MeshProtos.User import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.R +import com.geeksville.mesh.database.entity.Reaction val Routing.Error.stringRes: Int get() = when (this) { @@ -46,12 +47,14 @@ val Routing.Error.stringRes: Int data class Message( val uuid: Long, val receivedTime: Long, - val user: MeshProtos.User, + val user: User, val text: String, val time: String, val read: Boolean, val status: MessageStatus?, val routingError: Int, + val packetId: Int, + val emojis: List, ) { private fun getStatusStringRes(value: Int): Int { val error = Routing.Error.forNumber(value) ?: Routing.Error.UNRECOGNIZED diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 4ca7688c5..a8917719e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -61,7 +61,6 @@ import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.ServiceAction import com.geeksville.mesh.ui.map.MAP_STYLE_ID import com.geeksville.mesh.util.getShortDate -import com.geeksville.mesh.util.getShortDateTime import com.geeksville.mesh.util.positionToMeter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -330,20 +329,8 @@ class UIViewModel @Inject constructor( ) @OptIn(ExperimentalCoroutinesApi::class) - fun getMessagesFrom(contactKey: String) = packetRepository.getMessagesFrom(contactKey).mapLatest { list -> - list.map { - Message( - uuid = it.uuid, - receivedTime = it.received_time, - user = getUser(it.data.from), - text = it.data.text.orEmpty(), - time = getShortDateTime(it.data.time), - read = it.read, - status = it.data.status, - routingError = it.routingError, - ) - } - } + fun getMessagesFrom(contactKey: String) = packetRepository.getMessagesFrom(contactKey) + .mapLatest { list -> list.map { it.toMessage(::getUser) } } @OptIn(ExperimentalCoroutinesApi::class) val waypoints = packetRepository.getWaypoints().mapLatest { list -> @@ -386,10 +373,8 @@ class UIViewModel @Inject constructor( } } - fun sendTapback(emoji: String, replyId: Int, contactKey: String) { - viewModelScope.launch { - radioConfigRepository.onServiceAction(ServiceAction.Tapback(emoji, replyId, contactKey)) - } + fun sendReaction(emoji: String, replyId: Int, contactKey: String) = viewModelScope.launch { + radioConfigRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey)) } fun requestTraceroute(destNum: Int) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index b6d2b7731..2968785bb 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -43,6 +43,7 @@ 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.toNodeInfo import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.getTracerouteResponse @@ -76,7 +77,7 @@ import javax.inject.Inject import kotlin.math.absoluteValue sealed class ServiceAction { - data class Tapback(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction() + data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction() } /** @@ -302,7 +303,7 @@ class MeshService : Service(), Logging { .launchIn(serviceScope) radioConfigRepository.serviceAction.onEach { action -> when (action) { - is ServiceAction.Tapback -> sendTapback(action) + is ServiceAction.Reaction -> sendReaction(action) } }.launchIn(serviceScope) @@ -630,6 +631,16 @@ class MeshService : Service(), Logging { Portnums.PortNum.WAYPOINT_APP_VALUE, ) + private fun rememberReaction(packet: MeshPacket) = serviceScope.handledLaunch { + val reaction = ReactionEntity( + replyId = packet.decoded.replyId, + userId = toNodeID(packet.from), + emoji = packet.decoded.payload.toByteArray().decodeToString(), + timestamp = System.currentTimeMillis(), + ) + packetRepository.get().insertReaction(reaction) + } + private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) { if (dataPacket.dataType !in rememberDataType) return val fromLocal = dataPacket.from == DataPacket.ID_LOCAL @@ -682,8 +693,13 @@ class MeshService : Service(), Logging { when (data.portnumValue) { Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { - debug("Received CLEAR_TEXT from $fromId") - rememberDataPacket(dataPacket) + if (data.emoji != 0) { + debug("Received EMOJI from $fromId") + rememberReaction(packet) + } else { + debug("Received CLEAR_TEXT from $fromId") + rememberDataPacket(dataPacket) + } } Portnums.PortNum.WAYPOINT_APP_VALUE -> { @@ -1741,19 +1757,22 @@ class MeshService : Service(), Logging { } } - private fun sendTapback(tapback: ServiceAction.Tapback) = toRemoteExceptions { + private fun sendReaction(reaction: ServiceAction.Reaction) = toRemoteExceptions { // contactKey: unique contact key filter (channel)+(nodeId) - val channel = tapback.contactKey[0].digitToInt() - val destNum = tapback.contactKey.substring(1) + val channel = reaction.contactKey[0].digitToInt() + val destNum = reaction.contactKey.substring(1) - sendToRadio(newMeshPacketTo(destNum).buildMeshPacket( + val packet = newMeshPacketTo(destNum).buildMeshPacket( channel = channel, priority = MeshPacket.Priority.BACKGROUND, ) { - replyId = tapback.replyId + emoji = 1 + replyId = reaction.replyId portnumValue = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE - payload = ByteString.copyFrom(tapback.emoji.encodeToByteArray()) - }) + payload = ByteString.copyFrom(reaction.emoji.encodeToByteArray()) + } + sendToRadio(packet) + rememberReaction(packet.copy { from = myNodeNum }) } private val binder = object : IMeshService.Stub() { diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt b/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt index 5a6bbceec..08c58f16d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessageListView.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.geeksville.mesh.DataPacket import com.geeksville.mesh.model.Message +import com.geeksville.mesh.ui.components.ReactionRow import com.geeksville.mesh.ui.components.SimpleAlertDialog import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest @@ -46,6 +47,7 @@ internal fun MessageListView( selectedIds: MutableState>, onUnreadChanged: (Long) -> Unit, contentPadding: PaddingValues, + onSendReaction: (String, Int) -> Unit, onClick: (Message) -> Unit = {} ) { val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } } @@ -75,10 +77,12 @@ internal fun MessageListView( contentPadding = contentPadding ) { items(messages, key = { it.uuid }) { msg -> + val fromLocal = msg.user.id == DataPacket.ID_LOCAL val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } } + ReactionRow(fromLocal, msg.emojis) { onSendReaction(it, msg.packetId) } MessageItem( - shortName = msg.user.shortName.takeIf { msg.user.id != DataPacket.ID_LOCAL }, + shortName = msg.user.shortName.takeIf { !fromLocal }, messageText = msg.text, messageTime = msg.time, messageStatus = msg.status, diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 56a99b825..f021191c9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -251,7 +251,8 @@ internal fun MessageScreen( messages = messages, selectedIds = selectedIds, onUnreadChanged = { viewModel.clearUnreadCount(contactKey, it) }, - contentPadding = innerPadding + contentPadding = innerPadding, + onSendReaction = { emoji, id -> viewModel.sendReaction(emoji, id, contactKey) }, ) { // TODO onCLick() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt new file mode 100644 index 000000000..87336bfff --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.components + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter +import com.geeksville.mesh.util.CustomRecentEmojiProvider + +@Composable +fun EmojiPicker( + onDismiss: () -> Unit = {}, + onConfirm: (String) -> Unit +) { + Column( + verticalArrangement = Arrangement.Bottom + ) { + BackHandler { + onDismiss() + } + AndroidView( + factory = { context -> + androidx.emoji2.emojipicker.EmojiPickerView(context).apply { + clipToOutline = true + setRecentEmojiProvider( + RecentEmojiProviderAdapter(CustomRecentEmojiProvider(context)) + ) + setOnEmojiPickedListener { emoji -> + onDismiss() + onConfirm(emoji.emoji) + } + } + }, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(fraction = 0.4f) + .background(MaterialTheme.colors.background) + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/Reaction.kt b/app/src/main/java/com/geeksville/mesh/ui/components/Reaction.kt new file mode 100644 index 000000000..b6deecebe --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/Reaction.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2024 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Badge +import androidx.compose.material.BadgedBox +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.database.entity.Reaction +import com.geeksville.mesh.ui.theme.AppTheme + +@Composable +private fun ReactionItem( + emoji: String, + isAddEmojiItem: Boolean = false, + emojiCount: Int = 1, + onClick: () -> Unit = {}, +) { + BadgedBox( + modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp), + badge = { + if (emojiCount > 1) { + Badge( + backgroundColor = MaterialTheme.colors.onBackground, + contentColor = MaterialTheme.colors.background, + ) { + Text( + fontWeight = FontWeight.Bold, + text = emojiCount.toString() + ) + } + } + } + ) { + Surface( + modifier = Modifier + .clickable { onClick() }, + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(32.dp), + elevation = 4.dp, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (isAddEmojiItem) { + Icon( + imageVector = Icons.TwoTone.Add, + contentDescription = null, + modifier = Modifier.padding(start = 8.dp), + ) + } + Text( + text = emoji, + modifier = Modifier + .padding(8.dp) + .clip(CircleShape), + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ReactionRow( + fromLocal: Boolean, + reactions: List = emptyList(), + onSendReaction: (String) -> Unit = {} +) { + val emojiList by remember(reactions) { + mutableStateOf( + reduceEmojis( + if (fromLocal) { + reactions.map { it.emoji } + } else { + reactions.map { it.emoji }.reversed() + } + ).entries + ) + } + var showEmojiPickerDialog by remember { mutableStateOf(false) } + if (showEmojiPickerDialog) { + EmojiPickerDialog( + onConfirm = { + showEmojiPickerDialog = false + onSendReaction(it) + }, + onDismiss = { showEmojiPickerDialog = false } + ) + } + @Composable + fun AddEmojiItem() { + ReactionItem( + emoji = "\uD83D\uDE42", + isAddEmojiItem = true, + onClick = { + showEmojiPickerDialog = true + } + ) + } + + @Composable + fun EmojiList() { + emojiList.forEach { entry -> + ReactionItem( + emoji = entry.key, + emojiCount = entry.value, + onClick = { + onSendReaction(entry.key) + } + ) + } + } + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (fromLocal) Arrangement.End else Arrangement.Start + ) { + EmojiList() + AddEmojiItem() + } +} + +fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount() + +@Composable +fun EmojiPickerDialog( + onConfirm: (String) -> Unit, + onDismiss: () -> Unit = {}, +) { + Dialog( + onDismissRequest = onDismiss, + ) { + EmojiPicker( + onConfirm = onConfirm, + onDismiss = onDismiss, + ) + } +} + +@PreviewLightDark +@Composable +fun ReactionItemPreview() { + AppTheme { + Column( + modifier = Modifier.background(MaterialTheme.colors.background) + ) { + ReactionItem(emoji = "\uD83D\uDE42") + ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2) + ReactionItem(emoji = "\uD83D\uDE42", isAddEmojiItem = true) + } + } +} + +@Preview +@Composable +fun ReactionRowPreview() { + AppTheme { + ReactionRow( + fromLocal = true, reactions = listOf( + Reaction( + replyId = 1, + user = MeshProtos.User.getDefaultInstance(), + emoji = "\uD83D\uDE42", + timestamp = 1L + ), + Reaction( + replyId = 1, + user = MeshProtos.User.getDefaultInstance(), + emoji = "\uD83D\uDE42", + timestamp = 1L + ), + ) + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt index 33954af53..b1a035c3a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt @@ -17,7 +17,6 @@ package com.geeksville.mesh.ui.map -import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -25,7 +24,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -58,15 +56,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.emoji2.emojipicker.EmojiPickerView -import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.R import com.geeksville.mesh.copy import com.geeksville.mesh.ui.components.EditTextPreference +import com.geeksville.mesh.ui.components.EmojiPicker import com.geeksville.mesh.ui.theme.AppTheme -import com.geeksville.mesh.util.CustomRecentEmojiProvider import com.geeksville.mesh.waypoint @Suppress("LongMethod") @@ -184,31 +179,9 @@ internal fun EditWaypointDialog( } }, ) else { - Column( - verticalArrangement = Arrangement.Bottom - ) { - BackHandler { - showEmojiPickerView = false - } - - AndroidView( - factory = { context -> - EmojiPickerView(context).apply { - clipToOutline = true - setRecentEmojiProvider( - RecentEmojiProviderAdapter(CustomRecentEmojiProvider(context)) - ) - setOnEmojiPickedListener { emoji -> - showEmojiPickerView = false - waypointInput = waypointInput.copy { icon = emoji.emoji.codePointAt(0) } - } - } - }, - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.4f) - .background(MaterialTheme.colors.background) - ) + EmojiPicker(onDismiss = { showEmojiPickerView = false }) { + showEmojiPickerView = false + waypointInput = waypointInput.copy { icon = it.codePointAt(0) } } } } diff --git a/config/detekt/detekt-baseline.xml b/config/detekt/detekt-baseline.xml index 927f9a593..c41d1c503 100644 --- a/config/detekt/detekt-baseline.xml +++ b/config/detekt/detekt-baseline.xml @@ -133,7 +133,7 @@ FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt ForbiddenComment:MapFragment.kt$// TODO: Accept filename input param from user ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE - FunctionNaming:PacketDao.kt$PacketDao$@Query("DELETE FROM packet WHERE uuid=:uuid") fun _delete(uuid: Long) + FunctionNaming:PacketDao.kt$PacketDao$@Query("DELETE FROM packet WHERE uuid=:uuid") suspend fun _delete(uuid: Long) FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long) FunctionParameterNaming:LocationUtils.kt$_degIn: Double FunctionParameterNaming:LocationUtils.kt$lat_a: Double @@ -231,7 +231,6 @@ MagicNumber:EditListPreference.kt$12 MagicNumber:EditListPreference.kt$12345 MagicNumber:EditListPreference.kt$67890 - MagicNumber:EditWaypointDialog.kt$0.4f MagicNumber:EditWaypointDialog.kt$123 MagicNumber:EditWaypointDialog.kt$128169 MagicNumber:EditWaypointDialog.kt$128205 @@ -284,7 +283,6 @@ MagicNumber:MeshService.kt$MeshService$32 MagicNumber:MeshService.kt$MeshService$60000 MagicNumber:MeshService.kt$MeshService$8 - MagicNumber:MessagesFragment.kt$MessagesFragment$200 MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5 MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7 @@ -369,7 +367,6 @@ MagicNumber:TCPInterface.kt$TCPInterface$500 MagicNumber:UIState.kt$4 MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider - MatchingDeclarationName:CompatExtensions.kt$PendingIntentCompat MatchingDeclarationName:DistanceExtensions.kt$DistanceUnit MatchingDeclarationName:LocationUtils.kt$GPSFormat MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker @@ -484,7 +481,6 @@ MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" } MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings.add(it) } MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings[index] = it } - MultiLineIfElse:ChannelFragment.kt$item { PreferenceFooter( enabled = enabled, onCancelClicked = { focusManager.clearFocus() showChannelEditor = false channelSet = channels }, onSaveClicked = { focusManager.clearFocus() // viewModel.setRequestChannelUrl(channelUrl) sendButton() }) } MultiLineIfElse:ChannelOption.kt$when (bandwidth) { 31 -> .03125f 62 -> .0625f 200 -> .203125f 400 -> .40625f 800 -> .8125f 1600 -> 1.6250f else -> bandwidth / 1000f } MultiLineIfElse:ContextServices.kt$MaterialAlertDialogBuilder(this) .setTitle(title) .setMessage(rationale) .setNeutralButton(R.string.cancel) { _, _ -> } .setPositiveButton(R.string.accept) { _, _ -> invokeFun() } .show() MultiLineIfElse:ContextServices.kt$invokeFun() @@ -610,7 +606,6 @@ NoConsecutiveBlankLines:IRadioInterface.kt$ NoConsecutiveBlankLines:NOAAWmsTileSource.kt$NOAAWmsTileSource$ NoConsecutiveBlankLines:NodeInfo.kt$ - NoConsecutiveBlankLines:PositionTest.kt$ NoConsecutiveBlankLines:PreviewParameterProviders.kt$ NoConsecutiveBlankLines:SafeBluetooth.kt$ NoConsecutiveBlankLines:SafeBluetooth.kt$SafeBluetooth$ @@ -629,7 +624,6 @@ NoWildcardImports:SafeBluetooth.kt$import android.bluetooth.* NoWildcardImports:SafeBluetooth.kt$import kotlinx.coroutines.* NoWildcardImports:SettingsFragment.kt$import com.geeksville.mesh.android.* - NoWildcardImports:UIState.kt$import com.geeksville.mesh.* NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.* OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract ParameterListWrapping:AppPrefs.kt$FloatPref$(thisRef: AppPrefs, prop: KProperty<Float>) @@ -641,7 +635,6 @@ RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex ReturnCount:ChannelOption.kt$internal fun LoRaConfig.radioFreq(channelNum: Int): Float ReturnCount:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean - ReturnCount:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) SpacingAroundColon:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$: SpacingAroundCurly:AppPrefs.kt$FloatPref$} @@ -652,7 +645,6 @@ SpacingAroundRangeOperator:BatteryInfo.kt$.. StringTemplate:NodeInfo.kt$Position$${time} SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException - SwallowedException:ChannelFragment.kt$ex: Throwable SwallowedException:ChannelSet.kt$ex: Throwable SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception SwallowedException:Exceptions.kt$ex: Throwable @@ -672,7 +664,6 @@ TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception TooGenericExceptionCaught:ChannelFragment.kt$ex: Exception - TooGenericExceptionCaught:ChannelFragment.kt$ex: Throwable TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception TooGenericExceptionCaught:Exceptions.kt$ex: Throwable @@ -748,7 +739,6 @@ WildcardImport:SafeBluetooth.kt$import android.bluetooth.* WildcardImport:SafeBluetooth.kt$import kotlinx.coroutines.* WildcardImport:SettingsFragment.kt$import com.geeksville.mesh.android.* - WildcardImport:UIState.kt$import com.geeksville.mesh.* WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*