Skip to content

Commit

Permalink
feat: add emoji reactions to message bubbles (#1421)
Browse files Browse the repository at this point in the history
* Add tapback emojis to message bubbles

Added TapBackEmojiItem composable to display tapback emojis.
Included it in MessageItem composable for incoming messages.
Added a FlowRow to show tapback emojis below the message bubble.

* feat: Add EmojiPicker View

* feat: show emojis for local messages

* feat: Add emoji tapbacks to messages

This commit introduces the ability to send and receive emoji tapbacks for messages.

- Adds emoji and replyId fields to DataPacket.
- Adds emoji tapback support to the MeshService
- Modifies UIState to handle emojis in message lists.

* feat: store tapbacks in database

Store tapbacks in the database and display them in the message list.
- Add a new table to the database to store tapbacks.
- Add a new DAO method to insert and retrieve tapbacks.
- Update the message list UI to display tapbacks.

* refactor: relation db and other changes

---------

Co-authored-by: Andre K <[email protected]>
  • Loading branch information
jamesarich and andrekir authored Dec 3, 2024
1 parent b3f4929 commit 2234f5a
Show file tree
Hide file tree
Showing 15 changed files with 1,048 additions and 186 deletions.
515 changes: 515 additions & 0 deletions app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/14.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}

Expand Down Expand Up @@ -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)
}
}
Expand Down
179 changes: 91 additions & 88 deletions app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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 <https://www.gnu.org/licenses/>.
*/

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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -102,4 +103,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
suspend fun setMuteUntil(contacts: List<String>, until: Long) = withContext(Dispatchers.IO) {
packetDao.setMuteUntil(contacts, until)
}

suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) {
packetDao.insert(reaction)
}
}
56 changes: 38 additions & 18 deletions app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
"""
Expand All @@ -92,7 +93,8 @@ interface PacketDao {
ORDER BY received_time DESC
"""
)
fun getMessagesFrom(contact: String): Flow<List<Packet>>
@Transaction
fun getMessagesFrom(contact: String): Flow<List<PacketEntity>>

@Query(
"""
Expand All @@ -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<Long>)
suspend fun deletePackets(uuidList: List<Long>)

@Query(
"""
Expand All @@ -113,27 +115,42 @@ interface PacketDao {
AND contact_key IN (:contactList)
"""
)
fun deleteContacts(contactList: List<String>)
suspend fun deleteContacts(contactList: List<String>)

@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<Long>): List<Int>

@Query("DELETE FROM reactions WHERE reply_id IN (:packetIds)")
suspend fun deleteReactions(packetIds: List<Int>)

@Transaction
suspend fun deleteMessages(uuidList: List<Long>) {
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)) }
}
Expand All @@ -145,7 +162,7 @@ interface PacketDao {
ORDER BY received_time ASC
"""
)
fun getDataPackets(): List<DataPacket>
suspend fun getDataPackets(): List<DataPacket>

@Query(
"""
Expand All @@ -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<DataPacket>? =
suspend fun getQueuedPackets(): List<DataPacket>? =
getDataPackets().filter { it.status == MessageStatus.QUEUED }

@Query(
Expand All @@ -169,10 +186,10 @@ interface PacketDao {
ORDER BY received_time ASC
"""
)
fun getAllWaypoints(): List<Packet>
suspend fun getAllWaypoints(): List<Packet>

@Transaction
fun deleteWaypoint(id: Int) {
suspend fun deleteWaypoint(id: Int) {
val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid }
deleteMessages(uuidList)
}
Expand All @@ -184,7 +201,7 @@ interface PacketDao {
suspend fun getContactSettings(contact: String): ContactSettings?

@Upsert
fun upsertContactSettings(contacts: List<ContactSettings>)
suspend fun upsertContactSettings(contacts: List<ContactSettings>)

@Transaction
suspend fun setMuteUntil(contacts: List<String>, until: Long) {
Expand All @@ -194,4 +211,7 @@ interface PacketDao {
}
upsertContactSettings(contactList)
}

@Upsert
suspend fun insert(reaction: ReactionEntity)
}
Loading

0 comments on commit 2234f5a

Please sign in to comment.