diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 71501a81..cf291845 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -43,7 +43,7 @@ kotlin { } } -// jvm() + jvm() sourceSets { androidMain.dependencies { @@ -125,6 +125,9 @@ kotlin { jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) + + // The jvm counterpart to sqldelight-android-driver / -native-driver above. + implementation(libs.sqldelight.sqlite.driver) } // Only exists when the ios targets above were declared; the default hierarchy template // creates this source set from them. @@ -191,7 +194,7 @@ dependencies { // add("kspIosX64", libs.androidx.room.compiler) add("kspIosArm64", libs.androidx.room3.compiler) } -// add("kspJvm", libs.androidx.room3.compiler) + add("kspJvm", libs.androidx.room3.compiler) // Add any other platform target you use in your project, for example kspDesktop } diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventReceiptDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventReceiptDao.kt index 730db07c..07fde1f6 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventReceiptDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventReceiptDao.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface BroadcastNostrEventReceiptDao { @Query("SELECT * FROM BroadcastNostrEventReceipt") - fun getAllBroadcastNostrEventReceipts(): List + suspend fun getAllBroadcastNostrEventReceipts(): List @Query("SELECT * FROM BroadcastNostrEventReceipt WHERE id = :id") fun getBroadcastNostrEventReceiptById(id: Long): Flow diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventRequestDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventRequestDao.kt index 0f625335..97175d4e 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventRequestDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/BroadcastNostrEventRequestDao.kt @@ -12,10 +12,10 @@ import kotlin.time.Instant @Dao interface BroadcastNostrEventRequestDao { @Query("SELECT * FROM BroadcastNostrEventRequest") - fun getAllBroadcastNostrEventRequests(): List + suspend fun getAllBroadcastNostrEventRequests(): List @Query("SELECT * FROM BroadcastNostrEventRequest WHERE nostrEventId = :nostrEventId ORDER BY createdAt ASC") - fun getFirstBroadcastNostrEventRequestByNostrEventId(nostrEventId: String): BroadcastNostrEventRequest? + suspend fun getFirstBroadcastNostrEventRequestByNostrEventId(nostrEventId: String): BroadcastNostrEventRequest? // See NegentropySynchronizeRequestDao: the old `createdAt > :createdAt` bound a // once-evaluated `Clock.System.now()` for the life of the Flow. Because Instants are diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatMessageDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatMessageDao.kt index daa45b01..1758a1a9 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatMessageDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatMessageDao.kt @@ -16,19 +16,19 @@ interface ChatMessageDao { @Transaction @Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC") - fun getChatMessagesByChatRoomId(chatRoomId: String): List + suspend fun getChatMessagesByChatRoomId(chatRoomId: String): List @Query("SELECT * FROM ChatMessage WHERE giftWrapPayloadId = :giftWrapPayloadId ORDER BY createdAt DESC") - fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: HexKey): ChatMessage? + suspend fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: HexKey): ChatMessage? @Query("SELECT * FROM ChatMessage WHERE marmotInnerEventId = :marmotInnerEventId ORDER BY createdAt DESC") - fun getChatMessagesByMarmotInnerEventId(marmotInnerEventId: HexKey): ChatMessage? + suspend fun getChatMessagesByMarmotInnerEventId(marmotInnerEventId: HexKey): ChatMessage? @Query("SELECT * FROM ChatMessage WHERE marmotGroupEventId = :marmotGroupEventId ORDER BY createdAt DESC") - fun getChatMessagesByMarmotGroupEventId(marmotGroupEventId: HexKey): ChatMessage? + suspend fun getChatMessagesByMarmotGroupEventId(marmotGroupEventId: HexKey): ChatMessage? @Query("SELECT COUNT(*) FROM ChatMessage WHERE chatRoomId = :chatRoomId AND senderPublicKey = :senderPublicKey") - fun countChatMessagesBySenderPublicKey(chatRoomId: HexKey, senderPublicKey: HexKey): Int + suspend fun countChatMessagesBySenderPublicKey(chatRoomId: HexKey, senderPublicKey: HexKey): Int @Upsert suspend fun upsert(chatMessage: press.mantra.compose.database.model.ChatMessage): Long diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatRoomDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatRoomDao.kt index cfa6006b..ba65001c 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatRoomDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ChatRoomDao.kt @@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.Flow interface ChatRoomDao { @Transaction @Query("SELECT * FROM ChatRoom WHERE id = :id AND deletedAt IS NULL") - fun findChatRoomById(id: String): LocalChatRoom? + suspend fun findChatRoomById(id: String): LocalChatRoom? @Transaction @Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey AND deletedAt IS NULL") - fun getChatRoomListByUserPublicKey(userPublicKey: String): List + suspend fun getChatRoomListByUserPublicKey(userPublicKey: String): List @Transaction @Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey AND deletedAt IS NULL") diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ConnectionDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ConnectionDao.kt index 4399eabd..2c73acb2 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ConnectionDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ConnectionDao.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface ConnectionDao { @Query("SELECT * FROM Connection LIMIT :limit") - fun getAllConnections(limit: Int = 21): List + suspend fun getAllConnections(limit: Int = 21): List @Query("SELECT * FROM Connection WHERE (sourcePublicKey = :alicePublicKey AND destinationPublicKey = :bobPublicKey) OR (sourcePublicKey = :bobPublicKey AND destinationPublicKey = :alicePublicKey)") diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapMessageDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapMessageDao.kt index 7b71babd..19ff15f2 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapMessageDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapMessageDao.kt @@ -7,7 +7,7 @@ import androidx.room3.Upsert @Dao interface GiftWrapMessageDao { @Query("SELECT * FROM GiftWrapMessage") - fun getAllGiftWrapMessages(): List + suspend fun getAllGiftWrapMessages(): List @Upsert suspend fun upsert(giftWrapMessage: press.mantra.compose.database.model.GiftWrapMessage) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapPayloadDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapPayloadDao.kt index f881170d..1ff37322 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapPayloadDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapPayloadDao.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface GiftWrapPayloadDao { @Query("SELECT * FROM GiftWrapPayload") - fun getAllGiftWrapUnsigned(): List + suspend fun getAllGiftWrapUnsigned(): List @Query("SELECT * FROM GiftWrapPayload WHERE publicKey = :publicKey AND giftWrapSealId IS NULL") fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapSealDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapSealDao.kt index e939d480..a551c40a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapSealDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/GiftWrapSealDao.kt @@ -7,7 +7,7 @@ import androidx.room3.Upsert @Dao interface GiftWrapSealDao { @Query("SELECT * FROM GiftWrapSeal") - fun getAllGiftWrapSeals(): List + suspend fun getAllGiftWrapSeals(): List @Upsert suspend fun upsert(giftWrapSeal: press.mantra.compose.database.model.GiftWrapSeal) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/InReplyToRelationDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/InReplyToRelationDao.kt index a447a22f..df8c2ad8 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/InReplyToRelationDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/InReplyToRelationDao.kt @@ -8,10 +8,10 @@ import androidx.room3.Upsert @Dao interface InReplyToRelationDao { @Query("SELECT * FROM InReplyToRelation WHERE replyingNostrEventId = :replyingNostrEventId") - fun getRepostByReplyingNostrEventId(replyingNostrEventId: String): press.mantra.compose.database.model.InReplyToRelation? + suspend fun getRepostByReplyingNostrEventId(replyingNostrEventId: String): press.mantra.compose.database.model.InReplyToRelation? @Query("SELECT * FROM InReplyToRelation WHERE inReplyToNostrEventId = :inReplyToNostrEventId") - fun getRepostByInReplyToNostrEventId(inReplyToNostrEventId: String): press.mantra.compose.database.model.InReplyToRelation? + suspend fun getRepostByInReplyToNostrEventId(inReplyToNostrEventId: String): press.mantra.compose.database.model.InReplyToRelation? @Insert suspend fun insert(inReplyToRelation: press.mantra.compose.database.model.InReplyToRelation) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MarmotKeyPackageBundleDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MarmotKeyPackageBundleDao.kt index a42223d1..9fd408ae 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MarmotKeyPackageBundleDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/MarmotKeyPackageBundleDao.kt @@ -16,10 +16,10 @@ interface MarmotKeyPackageBundleDao { fun observeAllMarmotKeyPackageBundles(publicKey: HexKey): Flow> @Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey ORDER BY createdAt DESC") - fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List + suspend fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List @Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :id ORDER BY createdAt DESC") - fun getMarmotKeyPackageBundleById(id: String): MarmotKeyPackageBundle? + suspend fun getMarmotKeyPackageBundleById(id: String): MarmotKeyPackageBundle? @Upsert suspend fun upsert(marmotKeyPackageBundle: MarmotKeyPackageBundle) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeRequestDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeRequestDao.kt index cb0ee4cd..04298523 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeRequestDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeRequestDao.kt @@ -18,7 +18,7 @@ interface NegentropySynchronizeRequestDao { fun observeNegentropySynchronizeRequestByPurposeAndStatusCount(purpose: String, status: List): Flow @Upsert - fun upsert(negentropySynchronizeRequest: NegentropySynchronizeRequest) + suspend fun upsert(negentropySynchronizeRequest: NegentropySynchronizeRequest) /** * Upsert rather than insert-or-ignore. `computeId` buckets by minute, so an identical @@ -28,5 +28,5 @@ interface NegentropySynchronizeRequestDao { * hit the network, while still collapsing duplicates into a single row. */ @Upsert - fun insert(negentropySynchronizeRequests: List) + suspend fun insert(negentropySynchronizeRequests: List) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeResultDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeResultDao.kt index bb51e37f..dea93bec 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeResultDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NegentropySynchronizeResultDao.kt @@ -6,5 +6,5 @@ import androidx.room3.Upsert @Dao interface NegentropySynchronizeResultDao { @Upsert - fun upsert(negentropySynchronizeResult: press.mantra.compose.database.model.NegentropySynchronizeResult) + suspend fun upsert(negentropySynchronizeResult: press.mantra.compose.database.model.NegentropySynchronizeResult) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NostrEventDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NostrEventDao.kt index 1066c814..e813241b 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/NostrEventDao.kt @@ -73,13 +73,13 @@ interface NostrEventDao { * constraints they had no parameter for. */ @RawQuery - fun getNostrEventsMatchingFilter( + suspend fun getNostrEventsMatchingFilter( query: RoomRawQuery ): List @Transaction @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getNostrEvents( + suspend fun getNostrEvents( kinds: Array, since: Instant, limit: Int @@ -87,7 +87,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE content LIKE '%' || :search || '%' AND kind in (:kinds) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getFilteredNostrEvents( + suspend fun getFilteredNostrEvents( kinds: Array, search: String, since: Instant, @@ -96,7 +96,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) AND id in (:ids) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getFilteredNostrEvents( + suspend fun getFilteredNostrEvents( kinds: Array, ids: Array, since: Instant, @@ -105,7 +105,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE id in (:ids) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getNostrEventsByIds( + suspend fun getNostrEventsByIds( ids: Array, since: Instant, limit: Int @@ -114,7 +114,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE kind = ${GroupEvent.KIND} AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getMLSGroupEvents( + suspend fun getMLSGroupEvents( since: Instant, limit: Int ): List @@ -135,7 +135,7 @@ interface NostrEventDao { "AND (expiresAt IS NULL OR expiresAt > :now) " + "ORDER BY createdAt DESC LIMIT :limit" ) - fun getMarmotGroupEvents( + suspend fun getMarmotGroupEvents( chatRoomIds: Array, since: Instant, until: Instant, @@ -145,7 +145,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) AND pubKey in (:authors) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getAuthoredNostrEvents( + suspend fun getAuthoredNostrEvents( kinds: Array, authors: Array, since: Instant, @@ -154,7 +154,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE tags LIKE '%' || :publicKey || '%' AND kind in (:kinds) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getPublicKeyMentionedNostrEvents( + suspend fun getPublicKeyMentionedNostrEvents( kinds: Array, publicKey: HexKey, since: Instant, @@ -163,7 +163,7 @@ interface NostrEventDao { @Transaction @Query("SELECT * FROM NostrEvent WHERE tags LIKE '%' || :eventId || '%reply%' AND kind in (:kinds) AND createdAt > :since ORDER BY createdAt ASC LIMIT :limit") - fun getNostrEventReplies( + suspend fun getNostrEventReplies( kinds: Array, eventId: HexKey, since: Instant, @@ -171,17 +171,17 @@ interface NostrEventDao { ): List @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC") - fun getAllNostrEvents(kinds: Array): List + suspend fun getAllNostrEvents(kinds: Array): List @Transaction @Query("SELECT * FROM NostrEvent WHERE id = :id") fun observeNostrEventById(id: String): Flow @Query("SELECT * FROM NostrEvent WHERE id = :id") - fun getNostrEventById(id: String): NostrEvent? + suspend fun getNostrEventById(id: String): NostrEvent? @Query("SELECT * FROM NostrEvent WHERE pubKey = :publicKey AND kind = :kind ORDER BY createdAt DESC") - fun getNostrEventByPublicKeyAndKind(publicKey: HexKey, kind: Kind): NostrEvent? + suspend fun getNostrEventByPublicKeyAndKind(publicKey: HexKey, kind: Kind): NostrEvent? @Upsert suspend fun upsert(nostrEvent: NostrEvent) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ParticipantDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ParticipantDao.kt index fc681765..85c21cfb 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ParticipantDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ParticipantDao.kt @@ -9,10 +9,10 @@ import press.mantra.compose.database.model.Participant @Dao interface ParticipantDao { @Query("SELECT * FROM Participant WHERE participantPublicKey = :participantPublicKey") - fun findParticipantByPublicKey(participantPublicKey: String): List + suspend fun findParticipantByPublicKey(participantPublicKey: String): List @Query("SELECT * FROM Participant WHERE chatRoomId = :chatRoomId") - fun findParticipantsByChatRoomId(chatRoomId: String): List + suspend fun findParticipantsByChatRoomId(chatRoomId: String): List @Upsert suspend fun upsert(participants: List) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/PostDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/PostDao.kt index 77ef09e6..2beedae8 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/PostDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/PostDao.kt @@ -8,10 +8,10 @@ import androidx.room3.Upsert @Dao interface PostDao { @Query("SELECT * FROM Post") - fun getAllPosts(): List + suspend fun getAllPosts(): List @Query("SELECT * FROM Post WHERE id = :id") - fun getPostById(id: String): press.mantra.compose.database.model.Post? + suspend fun getPostById(id: String): press.mantra.compose.database.model.Post? @Insert suspend fun insert(post: press.mantra.compose.database.model.Post) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ProfileDao.kt index 91ba8e5c..13fbad43 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ProfileDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ProfileDao.kt @@ -13,26 +13,26 @@ import kotlin.time.Instant @Dao interface ProfileDao { @Query("SELECT * FROM Profile WHERE createdAt > :createdAt LIMIT :limit") - fun getAllProfiles(limit: Int = 21, createdAt: Instant = GENESIS_AT): List + suspend fun getAllProfiles(limit: Int = 21, createdAt: Instant = GENESIS_AT): List @Query("SELECT * FROM Profile WHERE publicKey NOT IN (:excludedPublicKeys) AND createdAt > :createdAt LIMIT :limit") - fun getAllProfilesExcludingOnesWithThesePublicKeys( + suspend fun getAllProfilesExcludingOnesWithThesePublicKeys( excludedPublicKeys: Array, limit: Int = 21, createdAt: Instant = GENESIS_AT ): List @Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit") - fun filterProfiles(searchTerm: String, limit: Int = 21): List + suspend fun filterProfiles(searchTerm: String, limit: Int = 21): List @Query("SELECT * FROM Profile WHERE publicKey = :publicKey") - fun getProfileByPublicKey(publicKey: String): Profile? + suspend fun getProfileByPublicKey(publicKey: String): Profile? @Query("SELECT * FROM Profile WHERE publicKey = :publicKey") fun observeProfileByPublicKey(publicKey: String): Flow @Query("SELECT * FROM Profile WHERE publicKey IN (:publicKeys)") - fun getProfileByPublicKeys(publicKeys: List): List + suspend fun getProfileByPublicKeys(publicKeys: List): List @Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey") fun observeProfileWithFollowersByPublicKey(publicKey: String): Flow diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/QuotedRelationDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/QuotedRelationDao.kt index 0913b55f..f29bc86d 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/QuotedRelationDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/QuotedRelationDao.kt @@ -8,10 +8,10 @@ import androidx.room3.Upsert @Dao interface QuotedRelationDao { @Query("SELECT * FROM QuotedRelation WHERE quotingNostrEventId = :quotingNostrEventId") - fun getRepostByQuotingNostrEventId(quotingNostrEventId: String): press.mantra.compose.database.model.QuotedRelation? + suspend fun getRepostByQuotingNostrEventId(quotingNostrEventId: String): press.mantra.compose.database.model.QuotedRelation? @Query("SELECT * FROM QuotedRelation WHERE quotedNostrEventId = :quotedNostrEventId") - fun getRepostByQuotedNostrEventId(quotedNostrEventId: String): press.mantra.compose.database.model.QuotedRelation? + suspend fun getRepostByQuotedNostrEventId(quotedNostrEventId: String): press.mantra.compose.database.model.QuotedRelation? @Insert suspend fun insert(quotedRelation: press.mantra.compose.database.model.QuotedRelation) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ReactionDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ReactionDao.kt index 7f6c26c9..f4c41430 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ReactionDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/ReactionDao.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface ReactionDao { @Query("SELECT * FROM Reaction") - fun getAllReactions(): List + suspend fun getAllReactions(): List @Query("SELECT * FROM Reaction WHERE id = :id") fun getReactionById(id: String): Flow diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RecentSearchDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RecentSearchDao.kt index dc87b1aa..07d4be44 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RecentSearchDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RecentSearchDao.kt @@ -12,10 +12,10 @@ interface RecentSearchDao { fun observeRecentSearchByQuery(query: String): Flow @Query("SELECT * FROM RecentSearch ORDER BY createdAt DESC LIMIT :limit ") - fun getAllRecentSearches(limit: Int = 21): List + suspend fun getAllRecentSearches(limit: Int = 21): List @Delete - fun deleteRecentSearch(recentSearch: press.mantra.compose.database.model.RecentSearch) + suspend fun deleteRecentSearch(recentSearch: press.mantra.compose.database.model.RecentSearch) @Upsert suspend fun upsert(recentSearch: press.mantra.compose.database.model.RecentSearch) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RelayDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RelayDao.kt index 629616e9..65feae1a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RelayDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RelayDao.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface RelayDao { @Query("SELECT * FROM Relay") - fun getAllPosts(): List + suspend fun getAllPosts(): List @Query("SELECT * FROM Relay WHERE publicKey = :publicKey") fun observePublicKeyRelays(publicKey: String): Flow> diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RepostedRelationDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RepostedRelationDao.kt index 7a47218f..bab0d88b 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RepostedRelationDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/RepostedRelationDao.kt @@ -7,13 +7,13 @@ import androidx.room3.Upsert @Dao interface RepostedRelationDao { @Query("SELECT * FROM RepostedRelation") - fun getAllReposts(): List + suspend fun getAllReposts(): List @Query("SELECT * FROM RepostedRelation WHERE repostingNostrEventId = :repostingNostrEventId") - fun getRepostByRepostingId(repostingNostrEventId: String): press.mantra.compose.database.model.RepostedRelation? + suspend fun getRepostByRepostingId(repostingNostrEventId: String): press.mantra.compose.database.model.RepostedRelation? @Query("SELECT * FROM RepostedRelation WHERE repostedNostrEventId = :repostedNostrEventId") - fun getRepostByRepostedId(repostedNostrEventId: String): press.mantra.compose.database.model.RepostedRelation? + suspend fun getRepostByRepostedId(repostedNostrEventId: String): press.mantra.compose.database.model.RepostedRelation? @Upsert suspend fun upsert(repostedRelation: press.mantra.compose.database.model.RepostedRelation) diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventRequestDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventRequestDao.kt index 36e2e00c..8620b38d 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventRequestDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventRequestDao.kt @@ -18,8 +18,8 @@ interface SynchronizeNostrEventRequestDao { fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String, status: List): Flow @Upsert - fun upsert(synchronizeNostrEventRequest: press.mantra.compose.database.model.SynchronizeNostrEventRequest) + suspend fun upsert(synchronizeNostrEventRequest: press.mantra.compose.database.model.SynchronizeNostrEventRequest) @Insert - fun insert(synchronizeNostrEventRequests: List) + suspend fun insert(synchronizeNostrEventRequests: List) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventResultDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventResultDao.kt index 9e5fa4f1..036efaa6 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventResultDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/SynchronizeNostrEventResultDao.kt @@ -6,5 +6,5 @@ import androidx.room3.Upsert @Dao interface SynchronizeNostrEventResultDao { @Upsert - fun upsert(synchronizeNostrEventResult: press.mantra.compose.database.model.SynchronizeNostrEventResult) + suspend fun upsert(synchronizeNostrEventResult: press.mantra.compose.database.model.SynchronizeNostrEventResult) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/UnsignedNostrEventDao.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/UnsignedNostrEventDao.kt index 926231cd..3a23775b 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/UnsignedNostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/dao/UnsignedNostrEventDao.kt @@ -12,10 +12,10 @@ import press.mantra.compose.database.model.UnsignedNostrEvent @Dao interface UnsignedNostrEventDao { @Query("SELECT * FROM UnsignedNostrEvent") - fun getAUnsignedNostrEvents(): List + suspend fun getAUnsignedNostrEvents(): List @Query("SELECT * FROM UnsignedNostrEvent") // TODO: where nostrEvent.unsignedNostrEventId is 0 - fun getAllUnprocessedUnsignedNostrEvents(): List + suspend fun getAllUnprocessedUnsignedNostrEvents(): List @Query("SELECT * FROM UnsignedNostrEvent WHERE id = :id") fun getUnsignedNostrEventById(id: Long): Flow @@ -32,7 +32,7 @@ interface UnsignedNostrEventDao { @Transaction @Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0") - fun getLocalAccounts(): List + suspend fun getLocalAccounts(): List @Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL ORDER BY kind ASC") fun observeUnsignedNostrEvents(publicKey: String): Flow diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt index 27cfca37..39581e60 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/repository/DatabaseNostrRepository.kt @@ -647,7 +647,7 @@ class DatabaseNostrRepository( .distinctBy { it.id } } - private fun matchNegentropicNostrEvents( + private suspend fun matchNegentropicNostrEvents( synchronizationFilter: SynchronizationFilter, applyLimits: Boolean, ): List { diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/AppVersion.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/AppVersion.jvm.kt new file mode 100644 index 00000000..a5e0d987 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/AppVersion.jvm.kt @@ -0,0 +1,16 @@ +package press.mantra.compose + +/** + * Android reads these from `BuildConfig`, which the android plugin generates. There is no + * equivalent for the jvm target, so this reads the jar manifest that `compose.desktop` + * writes when it packages a distribution, and falls back when running from a class + * directory -- from gradle, or a test -- where no manifest exists. + */ +actual object AppVersion { + actual val versionName: String + get() = AppVersion::class.java.`package`?.implementationVersion ?: "0.0.0-dev" + + /** No monotonic build number exists here the way `versionCode` does on android. */ + actual val versionCode: String + get() = "0" +} diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/Platform.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/Platform.jvm.kt new file mode 100644 index 00000000..8763703e --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/Platform.jvm.kt @@ -0,0 +1,7 @@ +package press.mantra.compose + +class JvmPlatform : Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JvmPlatform() diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/PlatformContext.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/PlatformContext.jvm.kt new file mode 100644 index 00000000..208a1a97 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/PlatformContext.jvm.kt @@ -0,0 +1,47 @@ +package press.mantra.compose + +import java.io.File +import java.util.Locale + +/** + * Android wraps a `Context`, which is where the database and files directories come from. + * The jvm has no such object, so the location is carried explicitly. + * + * This is deliberately *not* the same class as `fr.acinq.phoenix.utils.PlatformContext`, + * matching how android and ios keep the two apart -- `MainActivity` and + * `MainViewController` each construct both. A desktop entry point should hand the same + * directory to both so one install keeps one place on disk. + */ +actual class PlatformContext( + val applicationDir: File = defaultMantraDir(), +) + +/** + * The conventional per-user application data directory for the host os. + * + * Pointedly not `java.io.tmpdir`, which is what the abandoned Aux implementation used + * behind a TODO: most systems clear it on reboot, and the chat history and marmot group + * state live here. + */ +internal fun defaultMantraDir(): File { + val home = File(System.getProperty("user.home")) + val os = System.getProperty("os.name").orEmpty().lowercase(Locale.ROOT) + return when { + os.contains("win") -> + (System.getenv("LOCALAPPDATA") ?: System.getenv("APPDATA")) + ?.takeIf { it.isNotBlank() } + ?.let { File(it, APP_DIR_NAME) } + ?: File(home, "AppData/Local/$APP_DIR_NAME") + + os.contains("mac") || os.contains("darwin") -> + File(home, "Library/Application Support/$APP_DIR_NAME") + + else -> + System.getenv("XDG_DATA_HOME") + ?.takeIf { it.isNotBlank() } + ?.let { File(it, APP_DIR_NAME) } + ?: File(home, ".local/share/$APP_DIR_NAME") + } +} + +private const val APP_DIR_NAME = "mantra" diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/database/builder/PlatformDatabaseBuilder.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/database/builder/PlatformDatabaseBuilder.jvm.kt new file mode 100644 index 00000000..423ff6f3 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/database/builder/PlatformDatabaseBuilder.jvm.kt @@ -0,0 +1,21 @@ +package press.mantra.compose.database.builder + +import androidx.room3.Room +import androidx.room3.RoomDatabase +import press.mantra.compose.PlatformContext +import press.mantra.compose.database.MantraDatabase +import java.io.File + +actual object PlatformDatabaseBuilder { + actual fun getDatabaseBuilder(platformContext: PlatformContext): RoomDatabase.Builder { + // Under the context's application directory, not java.io.tmpdir -- which is what the + // abandoned Aux implementation did behind a TODO, and which most systems clear on + // reboot. The file keeps android's name so one app does not have two on disk. + val dbFile = File(platformContext.applicationDir.apply { mkdirs() }, "aux.db") + return Room.databaseBuilder(name = dbFile.absolutePath) + } + + actual fun getInMemoryDatabaseBuilder(platform: PlatformContext): RoomDatabase.Builder { + return Room.inMemoryDatabaseBuilder() + } +} diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/extensions/Phoenix.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/extensions/Phoenix.jvm.kt new file mode 100644 index 00000000..d47aa7ec --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/extensions/Phoenix.jvm.kt @@ -0,0 +1,52 @@ +package press.mantra.compose.extensions + +import co.touchlab.kermit.Logger +import fr.acinq.bitcoin.Chain +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.data.StartBusinessResult +import fr.acinq.phoenix.jvm.BusinessManager +import fr.acinq.phoenix.managers.DataStoreManager +import fr.acinq.phoenix.utils.preferences.GlobalPrefs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +/** + * `Dispatchers.Default`, following android rather than ios -- ios hops to Main, and there is + * no reason for the jvm to start a lightning node on the ui thread. + */ +actual suspend fun platformStartupLogic(words: List): StartBusinessResult { + return withContext(Dispatchers.Default) { + BusinessManager.startNewBusiness(words, isHeadless = false) + } +} + +/** + * **Nothing is scheduled, and that has a consequence worth stating.** + * + * Android schedules two WorkManager jobs here: `ChannelsWatcher`, which wakes periodically + * to check whether a channel has been force-closed while the app was shut, and + * `ContactsPhotoCleaner`. Ios schedules neither and logs that it does not. + * + * The jvm has no equivalent, and not for want of an api -- a desktop application simply has + * no process once its window closes, so there is nothing for a scheduler to wake. Running + * the watcher in-process would be worse than not running it: it would only ever fire while + * the app was already open and watching anyway, which is the case that needs no help. + * + * The exposure is real. A desktop wallet that stays closed past a force-close deadline does + * not notice. Anything that closes it -- a system daemon, a scheduled task -- has to live + * outside this process, which is why this logs rather than pretending. + */ +actual fun schedulePlatformLogic(phoenixGlobal: PhoenixGlobal) { + Logger.withTag("schedulePlatformLogic").w { + "no background scheduling on the jvm target: channels are only watched while the app is open" + } +} + +actual fun getShowIntroFlow(phoenixGlobal: PhoenixGlobal): Flow { + return DataStoreManager(phoenixGlobal.ctx, Chain.Mainnet).loadGlobalPrefsForWallet().getShowIntro +} + +actual fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs { + return DataStoreManager(phoenixGlobal.ctx, Chain.Mainnet).loadGlobalPrefsForWallet() +} diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/theme/Theme.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/theme/Theme.jvm.kt new file mode 100644 index 00000000..c5d5e191 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/theme/Theme.jvm.kt @@ -0,0 +1,17 @@ +package press.mantra.compose.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +/** + * `dynamicColor` is ignored: it means Material You, which reads a wallpaper-derived palette + * from the android system and has no desktop counterpart. The app's own schemes are used + * whatever the caller asks for. + */ +@Composable +actual fun themeColorScheme( + darkTheme: Boolean, + dynamicColor: Boolean, + darkScheme: ColorScheme, + lightScheme: ColorScheme +): ColorScheme = if (darkTheme) darkScheme else lightScheme diff --git a/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.jvm.kt b/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.jvm.kt new file mode 100644 index 00000000..62d6645a --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/press/mantra/compose/ui/view/model/NavigationViewModel.jvm.kt @@ -0,0 +1,157 @@ +// The ios actuals with one import changed. That file uses no ios API -- SeedManager, +// DataStoreManager, EncryptedSeed and LocalKeyManager are all commonMain -- so the only +// thing that had to move was BusinessManager, which is now ported to the library's jvmMain. +// +// Kept as a copy for the same reason the ported BusinessManager is: sharing it means an +// intermediate source set between iosMain and jvmMain, which is a change to how the module +// is wired rather than to what it does. + +package press.mantra.compose.ui.view.model + +import press.mantra.compose.AppVersion +import co.touchlab.kermit.Logger +import fr.acinq.bitcoin.Chain +import fr.acinq.bitcoin.MnemonicCode +import fr.acinq.lightning.crypto.LocalKeyManager +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.phoenix.PhoenixGlobal +import fr.acinq.phoenix.data.DecryptSeedResult +import fr.acinq.phoenix.data.ElectrumConfig +import fr.acinq.phoenix.data.WalletId +import fr.acinq.phoenix.jvm.BusinessManager +import fr.acinq.phoenix.managers.DataStoreManager +import fr.acinq.phoenix.managers.NodeParamsManager +import fr.acinq.phoenix.managers.SeedManager +import fr.acinq.phoenix.security.EncryptedSeed +import fr.acinq.phoenix.utils.preferences.GlobalPrefs +import fr.acinq.phoenix.utils.preferences.UserWalletMetadata +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + + +actual fun updateBusinessActiveInUI(walletId: WalletId) { + BusinessManager.updateBusinessActiveInUI(walletId) +} + +actual fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult { + return SeedManager.loadAndDecrypt( + phoenixGlobal + ) +} + +actual fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow> { + return DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().getAvailableWalletsMeta +} + +actual suspend fun saveAvailableWalletMeta( + phoenixGlobal: PhoenixGlobal, + metadata: UserWalletMetadata +) { + DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().saveAvailableWalletMeta(metadata) +} + +actual suspend fun saveAvailableWalletMeta( + phoenixGlobal: PhoenixGlobal, + walletId: WalletId, + name: String?, + avatar: String, + isHidden: Boolean +) { + DataStoreManager( + phoenixGlobal.ctx, + chain = Chain.Mainnet + ).loadGlobalPrefsForWallet().saveAvailableWalletMeta( + walletId = walletId, + name = name, + avatar = avatar, + isHidden = isHidden + ) +} + +actual fun platformWriteSeed( + log: Logger, + phoenixGlobal: PhoenixGlobal, + globalPrefs: GlobalPrefs, + writingState: WritingSeedState, + viewModelScope: CoroutineScope, + mnemonics: List, + onWritingSeedError: (WritingSeedState.Error) -> Unit, + onWritingSeedStateWriting: (WritingSeedState.Writing) -> Unit, + isRestoringWallet: Boolean, + isTorEnabled: Boolean, + customElectrumServer: ElectrumConfig.Custom?, + onSeedWritten: (WalletId) -> Unit +) { + if (writingState !is WritingSeedState.Init) return + viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e -> + log.e("failed to write mnemonics to disk: ${e.message}") + onWritingSeedError.invoke( + WritingSeedState.Error.Generic(e) + ) + }) { + log.d("writing mnemonics to disk...") + + onWritingSeedStateWriting.invoke( + WritingSeedState.Writing(mnemonics) + ) + val existingSeeds = SeedManager.loadAndDecryptOrNull(phoenixGlobal)?.map { + it.key to it.value.words + }?.toMap() + + val seed = MnemonicCode.toSeed(mnemonics, "").toByteVector() + val keyManager = LocalKeyManager(seed, NodeParamsManager.chain, NodeParamsManager.remoteSwapInXpub) + val newWalletId = WalletId(keyManager.nodeKeys.nodeKey.publicKey) + + when { + existingSeeds == null -> { + log.e("could not load the existing seed map, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.CannotLoadSeedMap + ) + return@launch + } + existingSeeds.containsKey(newWalletId) -> { + log.i("attempting to import a seed that already exists, aborting...") + onWritingSeedError.invoke( + WritingSeedState.Error.SeedAlreadyExists + ) + return@launch + } + else -> { + val newSeedMap = existingSeeds + (newWalletId to mnemonics) + val encrypted = EncryptedSeed.V2.encrypt(newSeedMap) + SeedManager.writeSeedToDisk(phoenixGlobal, encrypted, overwrite = true) + if (isRestoringWallet) { + log.i("successfully restored wallet=$newWalletId") + } else { + log.i("successfully created wallet=$newWalletId") + } + } + } + + globalPrefs.saveLastUsedAppCode(AppVersion.versionCode) + val dataStoreManager = DataStoreManager( + phoenixGlobal.ctx, + chain = NodeParamsManager.chain, + ) + val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = newWalletId) + userPrefs.saveIsTorEnabled(isTorEnabled) + userPrefs.saveElectrumServer(customElectrumServer) + + viewModelScope.launch(Dispatchers.Main) { + delay(1000) + onSeedWritten(newWalletId) + } + } +} \ No newline at end of file diff --git a/docs/jvm-target.md b/docs/jvm-target.md index a9314379..e5368ddc 100644 --- a/docs/jvm-target.md +++ b/docs/jvm-target.md @@ -424,6 +424,37 @@ Those two go together: the KSP configuration does not exist until the target does, which is why Phase 0 deliberately left it alone. Then let the compiler drive. +### Before any of that: Room will not generate a DAO for a non-android target + +**This is the real content of Phase 4, and it is not the actuals.** The first jvm +compile fails with 58 copies of: + +``` +Only suspend functions are allowed in DAOs declared in source sets targeting +non-Android platforms. +``` + +Room permits blocking query methods **only** on android. Every `@Dao` function that +is neither `suspend` nor `Flow`-returning has to change, and there were 58 of them +across 25 files in `database/dao/`. KSP reports them in alphabetical batches, so the +count shrinks in stages and looks endless; scan for them directly instead — an +abstract `fun` in a `@Dao` that has no `suspend` and no `Flow<...>` return. + +The saving grace is that it stops there. All 15 call sites outside the DAO layer +were **already inside `suspend` functions** — the repositories were written that way +throughout — so the change is `suspend` on 58 declarations, plus exactly one private +helper (`DatabaseNostrRepository.matchNegentropicNostrEvents`), whose single caller +was already suspend. Zero call-site edits. + +It is not free, though, and the cost lands on **android**. A blocking DAO method runs +on its caller's thread; a `suspend` one is dispatched to the query coroutine context, +which `getRoomDatabase` sets to `Dispatchers.IO`. That is the better behaviour — it is +what stops a query running on the main thread — but it is a real change to a shipping +platform, made for the benefit of a target that does not exist yet. Run the android +unit tests, not just the compile. + +### Then the actuals + Mantra declares 16 expects across 8 files. They split cleanly: **Six platform basics.** `getPlatform` ([Platform.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/Platform.kt)), @@ -442,6 +473,19 @@ directory from Phase 1 — not `java.io.tmpdir`, which is what the old Aux implementation did and which silently loses the database on reboot on most systems. +**The library's android- and ios-only classes are not all expects.** The 23 counted +at the top of this document are `expect` declarations, and the compiler lists those +for you. `BusinessManager` is not one — it exists as a plain object in `androidMain` +and again in `iosMain`, with no common declaration, so nothing flags its absence until +mantra's own actual tries to import it. + +The ios one turns out to contain no ios API whatever: no `platform.*`, no cinterop, no +`NSObject`. It ports to `jvmMain` on a package rename alone and compiles unchanged. It +is also the right one to start from — the android manager is built around an android +`Application` it holds, while the ios one constructs its own `PlatformContext`, which +is exactly what the jvm can do. `NavigationViewModel.ios.kt` is likewise portable on +one changed import. + **Nine lightning wrappers.** Four in [Phoenix.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/extensions/Phoenix.kt) (`platformStartupLogic`, `schedulePlatformLogic`, `getShowIntroFlow`, @@ -454,10 +498,17 @@ android actuals live in `NavigationViewModel.android.kt`. These are thin — they mostly forward into the phoenix library. They are thin *because* Phases 1–3 did the work, which is why they are last. -`schedulePlatformLogic` is the one to look at properly: on android it schedules -background work through WorkManager. On desktop there is no equivalent and no -process that outlives the window. Decide explicitly whether it becomes a no-op or -an in-process coroutine, and write down which. +`schedulePlatformLogic` is the one to look at properly, and the answer is that it +should schedule **nothing** and say so. On android it starts two WorkManager jobs, one +of which is `ChannelsWatcher` — it wakes periodically to notice a channel force-closed +while the app was shut. A desktop application has no process once its window closes, +so there is nothing for a scheduler to wake, and running the watcher in-process would +be strictly worse than not running it: it would only ever fire while the app was +already open and watching anyway. + +The exposure is real and belongs in the release notes, not just a comment: a desktop +wallet left closed past a force-close deadline does not notice. Covering it needs +something outside this process, which is a separate piece of work from this plan. **Verification:** `./gradlew :composeApp:compileKotlinJvm`. This is the first point in the plan where the JVM target has to actually resolve, so expect the diff --git a/lightning-kmp-app b/lightning-kmp-app index 64342823..05ce7eb3 160000 --- a/lightning-kmp-app +++ b/lightning-kmp-app @@ -1 +1 @@ -Subproject commit 6434282380a14a997a2ac91227bb4d787a6f2c76 +Subproject commit 05ce7eb3ffbe0e7dfd7e0b2b31bbfde84cf0efa8