Skip to content

Commit 449cbd4

Browse files
jamesarichCopilot
andauthored
feat: FTS5 full-text message search (#5373)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0951720 commit 449cbd4

20 files changed

Lines changed: 1554 additions & 2 deletions

File tree

.skills/compose-ui/strings-index.txt

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

androidApp/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,6 @@ dependencies {
305305
testImplementation(libs.compose.multiplatform.ui.test)
306306
testImplementation(libs.androidx.test.ext.junit)
307307
testImplementation(libs.androidx.glance.appwidget)
308+
// JVM variant provides the host-platform native library for BundledSQLiteDriver under Robolectric
309+
testRuntimeOnly("androidx.sqlite:sqlite-bundled-jvm:2.6.2")
308310
}

core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.paging.PagingData
2222
import androidx.paging.map
2323
import kotlinx.coroutines.flow.Flow
2424
import kotlinx.coroutines.flow.flatMapLatest
25+
import kotlinx.coroutines.flow.flowOf
2526
import kotlinx.coroutines.flow.map
2627
import kotlinx.coroutines.flow.mapLatest
2728
import kotlinx.coroutines.withContext
@@ -139,6 +140,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
139140
rssi = packet.rssi,
140141
hopsAway = packet.hopsAway,
141142
filtered = filtered,
143+
messageText = packet.text.orEmpty(),
142144
)
143145
insertRoomPacket(packetToSave)
144146
}
@@ -278,6 +280,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
278280
rssi = packet.rssi,
279281
hopsAway = packet.hopsAway,
280282
filtered = filtered,
283+
messageText = packet.text.orEmpty(),
281284
)
282285
insertRoomPacket(packetToSave)
283286
}
@@ -510,6 +513,54 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
510513
sfpp_hash = sfppHash,
511514
)
512515

516+
override fun searchMessages(query: String, contactKey: String?, getNode: (String?) -> Node): Flow<List<Message>> {
517+
val sanitized = sanitizeFtsQuery(query)
518+
if (sanitized.isBlank()) return flowOf(emptyList())
519+
return dbManager.currentDb.flatMapLatest { db ->
520+
kotlinx.coroutines.flow.flow {
521+
val dao = db.packetDao()
522+
val packets =
523+
if (contactKey != null) {
524+
dao.searchMessagesInConversation(sanitized, contactKey)
525+
} else {
526+
dao.searchMessages(sanitized)
527+
}
528+
emit(
529+
packets.map { packet ->
530+
val node = getNode(packet.data.from)
531+
val isFromLocal =
532+
node.user.id == DataPacket.ID_LOCAL ||
533+
(packet.myNodeNum != 0 && node.num == packet.myNodeNum)
534+
Message(
535+
uuid = packet.uuid,
536+
receivedTime = packet.received_time,
537+
node = node,
538+
text = packet.data.text.orEmpty(),
539+
fromLocal = isFromLocal,
540+
time = org.meshtastic.core.model.util.getShortDateTime(packet.data.time),
541+
snr = packet.snr,
542+
rssi = packet.rssi,
543+
hopsAway = packet.hopsAway,
544+
read = packet.read,
545+
status = packet.data.status,
546+
routingError = packet.routingError,
547+
packetId = packet.packetId,
548+
emojis = emptyList(),
549+
replyId = packet.data.replyId,
550+
)
551+
},
552+
)
553+
}
554+
}
555+
}
556+
557+
/**
558+
* Sanitizes a user query for FTS5 by wrapping each token in double quotes. This escapes FTS5 special characters (*,
559+
* -, NEAR, etc.) while still allowing multi-word searches as implicit AND queries.
560+
*/
561+
private fun sanitizeFtsQuery(query: String): String =
562+
query.split("\\s+".toRegex()).filter { it.isNotBlank() }.joinToString(" ") { "\"${it.replace("\"", "")}\"" }
563+
513564
companion object {
514565
private const val CONTACTS_PAGE_SIZE = 30
515566
private const val MESSAGES_PAGE_SIZE = 50

core/database/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ kotlin {
5353
val androidHostTest by getting {
5454
dependencies {
5555
implementation(libs.androidx.sqlite.bundled)
56+
// JVM variant provides the host-platform native for BundledSQLiteDriver
57+
runtimeOnly("androidx.sqlite:sqlite-bundled-jvm:2.6.2")
5658
implementation(libs.androidx.room.testing)
5759
implementation(libs.androidx.test.ext.junit)
5860
implementation(libs.junit)

0 commit comments

Comments
 (0)