feat: mantra compiles for the jvm
Phase 4. Declares jvm(), implements all 16 expects, and bumps the submodule to the fork branch carrying phases 1-3. :composeApp:compileKotlinJvm is green. **The actuals were the small half. Room was the blocker.** The first jvm compile failed with 58 copies of "Only suspend functions are allowed in DAOs declared in source sets targeting non-Android platforms". Room permits blocking query methods on android and nowhere else, so every @Dao function that was neither suspend nor Flow-returning had to change -- 58 of them across 24 files. KSP reports these in alphabetical batches, so the count shrinks in stages and looks bottomless; scanning the dao package directly for abstract funs with no suspend and no Flow return finds them all at once. It stops there, which is the only reason this is a 58-line change rather than a refactor. Every one of the 15 call sites outside the dao package was already inside a suspend function -- the repositories were written that way throughout -- so nothing needed rewriting. One private helper, DatabaseNostrRepository.matchNegentropicNostrEvents, had to become suspend, and its single caller was already suspend, so the cascade terminated immediately. Zero call-site edits. **The cost lands on android, not on the jvm.** 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 the shipping platform, made for a target that does not run yet. Hence the unit tests below rather than a compile alone. **BusinessManager was not an expect**, so nothing warned about it. It is now ported to the fork's jvmMain (05ce7eb); Phoenix.jvm.kt and NavigationViewModel.jvm.kt are otherwise the ios actuals with one changed import, since those files use no ios API. **schedulePlatformLogic schedules nothing, and logs that it does not.** Android starts two WorkManager jobs here, 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 to wake, and running the watcher in-process would be strictly worse than not running it: it would only fire while the app was already open and watching. The exposure is real and belongs in release notes rather than a comment -- a desktop wallet left closed past a force-close deadline does not notice. Smaller calls. PlatformContext carries an application directory, since there is no Context to read one from, and PlatformDatabaseBuilder puts aux.db under it rather than in java.io.tmpdir, which is what the abandoned Aux implementation did behind a TODO and which most systems clear on reboot. themeColorScheme ignores dynamicColor, which means Material You and has no desktop counterpart. AppVersion reads the jar manifest that compose.desktop writes, falling back when running from a class directory. Verified: :composeApp:compileKotlinJvm green, :composeApp:compileDebugKotlinAndroid green, and :composeApp:testDebugUnitTest 52 passing -- the one that matters, since this commit changes shared code every android query path goes through. Not verified: nothing has run. No jvm entry point exists yet, so the database has never been opened on this platform and no business has been started. That is phase 5, which also has to unlock JvmKeyStore before the wallet starts -- a passphrase prompt, not just a window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
670f87a609
commit
bf4041a5b2
35 files changed
+437
-66
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface BroadcastNostrEventReceiptDao {
|
||||
@Query("SELECT * FROM BroadcastNostrEventReceipt")
|
||||
fun getAllBroadcastNostrEventReceipts(): List<press.mantra.compose.database.model.BroadcastNostrEventReceipt>
|
||||
suspend fun getAllBroadcastNostrEventReceipts(): List<press.mantra.compose.database.model.BroadcastNostrEventReceipt>
|
||||
|
||||
@Query("SELECT * FROM BroadcastNostrEventReceipt WHERE id = :id")
|
||||
fun getBroadcastNostrEventReceiptById(id: Long): Flow<press.mantra.compose.database.model.BroadcastNostrEventReceipt?>
|
||||
|
||||
+2
-2
@@ -12,10 +12,10 @@ import kotlin.time.Instant
|
||||
@Dao
|
||||
interface BroadcastNostrEventRequestDao {
|
||||
@Query("SELECT * FROM BroadcastNostrEventRequest")
|
||||
fun getAllBroadcastNostrEventRequests(): List<BroadcastNostrEventRequest>
|
||||
suspend fun getAllBroadcastNostrEventRequests(): List<BroadcastNostrEventRequest>
|
||||
|
||||
@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
|
||||
|
||||
+5
-5
@@ -16,19 +16,19 @@ interface ChatMessageDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ChatMessage WHERE chatRoomId = :chatRoomId ORDER BY createdAt DESC")
|
||||
fun getChatMessagesByChatRoomId(chatRoomId: String): List<press.mantra.compose.database.model.intermdiate.LocalChatMessage>
|
||||
suspend fun getChatMessagesByChatRoomId(chatRoomId: String): List<press.mantra.compose.database.model.intermdiate.LocalChatMessage>
|
||||
|
||||
@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
|
||||
|
||||
@@ -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<LocalChatRoom>
|
||||
suspend fun getChatRoomListByUserPublicKey(userPublicKey: String): List<LocalChatRoom>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey AND deletedAt IS NULL")
|
||||
|
||||
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface ConnectionDao {
|
||||
@Query("SELECT * FROM Connection LIMIT :limit")
|
||||
fun getAllConnections(limit: Int = 21): List<press.mantra.compose.database.model.Connection>
|
||||
suspend fun getAllConnections(limit: Int = 21): List<press.mantra.compose.database.model.Connection>
|
||||
|
||||
|
||||
@Query("SELECT * FROM Connection WHERE (sourcePublicKey = :alicePublicKey AND destinationPublicKey = :bobPublicKey) OR (sourcePublicKey = :bobPublicKey AND destinationPublicKey = :alicePublicKey)")
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import androidx.room3.Upsert
|
||||
@Dao
|
||||
interface GiftWrapMessageDao {
|
||||
@Query("SELECT * FROM GiftWrapMessage")
|
||||
fun getAllGiftWrapMessages(): List<press.mantra.compose.database.model.GiftWrapMessage>
|
||||
suspend fun getAllGiftWrapMessages(): List<press.mantra.compose.database.model.GiftWrapMessage>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(giftWrapMessage: press.mantra.compose.database.model.GiftWrapMessage)
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface GiftWrapPayloadDao {
|
||||
@Query("SELECT * FROM GiftWrapPayload")
|
||||
fun getAllGiftWrapUnsigned(): List<GiftWrapPayload>
|
||||
suspend fun getAllGiftWrapUnsigned(): List<GiftWrapPayload>
|
||||
|
||||
@Query("SELECT * FROM GiftWrapPayload WHERE publicKey = :publicKey AND giftWrapSealId IS NULL")
|
||||
fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow<GiftWrapPayload?>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import androidx.room3.Upsert
|
||||
@Dao
|
||||
interface GiftWrapSealDao {
|
||||
@Query("SELECT * FROM GiftWrapSeal")
|
||||
fun getAllGiftWrapSeals(): List<press.mantra.compose.database.model.GiftWrapSeal>
|
||||
suspend fun getAllGiftWrapSeals(): List<press.mantra.compose.database.model.GiftWrapSeal>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(giftWrapSeal: press.mantra.compose.database.model.GiftWrapSeal)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@ interface MarmotKeyPackageBundleDao {
|
||||
fun observeAllMarmotKeyPackageBundles(publicKey: HexKey): Flow<List<MarmotKeyPackageBundle>>
|
||||
|
||||
@Query("SELECT * FROM MarmotKeyPackageBundle WHERE publicKey = :publicKey ORDER BY createdAt DESC")
|
||||
fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
|
||||
suspend fun getAllMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
|
||||
|
||||
@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)
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ interface NegentropySynchronizeRequestDao {
|
||||
fun observeNegentropySynchronizeRequestByPurposeAndStatusCount(purpose: String, status: List<String>): Flow<Int>
|
||||
|
||||
@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<NegentropySynchronizeRequest>)
|
||||
suspend fun insert(negentropySynchronizeRequests: List<NegentropySynchronizeRequest>)
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
+13
-13
@@ -73,13 +73,13 @@ interface NostrEventDao {
|
||||
* constraints they had no parameter for.
|
||||
*/
|
||||
@RawQuery
|
||||
fun getNostrEventsMatchingFilter(
|
||||
suspend fun getNostrEventsMatchingFilter(
|
||||
query: RoomRawQuery
|
||||
): List<NostrEvent>
|
||||
|
||||
@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<Kind>,
|
||||
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<Kind>,
|
||||
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<Kind>,
|
||||
ids: Array<HexKey>,
|
||||
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<HexKey>,
|
||||
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<NostrEvent>
|
||||
@@ -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<HexKey>,
|
||||
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<Kind>,
|
||||
authors: Array<HexKey>,
|
||||
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<Kind>,
|
||||
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<Kind>,
|
||||
eventId: HexKey,
|
||||
since: Instant,
|
||||
@@ -171,17 +171,17 @@ interface NostrEventDao {
|
||||
): List<NostrEvent>
|
||||
|
||||
@Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC")
|
||||
fun getAllNostrEvents(kinds: Array<Kind>): List<NostrEvent>
|
||||
suspend fun getAllNostrEvents(kinds: Array<Kind>): List<NostrEvent>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM NostrEvent WHERE id = :id")
|
||||
fun observeNostrEventById(id: String): Flow<press.mantra.compose.database.model.intermdiate.LocalNostrEvent?>
|
||||
|
||||
@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)
|
||||
|
||||
+2
-2
@@ -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<Participant>
|
||||
suspend fun findParticipantByPublicKey(participantPublicKey: String): List<Participant>
|
||||
|
||||
@Query("SELECT * FROM Participant WHERE chatRoomId = :chatRoomId")
|
||||
fun findParticipantsByChatRoomId(chatRoomId: String): List<Participant>
|
||||
suspend fun findParticipantsByChatRoomId(chatRoomId: String): List<Participant>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(participants: List<Participant>)
|
||||
|
||||
@@ -8,10 +8,10 @@ import androidx.room3.Upsert
|
||||
@Dao
|
||||
interface PostDao {
|
||||
@Query("SELECT * FROM Post")
|
||||
fun getAllPosts(): List<press.mantra.compose.database.model.intermdiate.LocalPost>
|
||||
suspend fun getAllPosts(): List<press.mantra.compose.database.model.intermdiate.LocalPost>
|
||||
|
||||
@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)
|
||||
|
||||
@@ -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<Profile>
|
||||
suspend fun getAllProfiles(limit: Int = 21, createdAt: Instant = GENESIS_AT): List<Profile>
|
||||
|
||||
@Query("SELECT * FROM Profile WHERE publicKey NOT IN (:excludedPublicKeys) AND createdAt > :createdAt LIMIT :limit")
|
||||
fun getAllProfilesExcludingOnesWithThesePublicKeys(
|
||||
suspend fun getAllProfilesExcludingOnesWithThesePublicKeys(
|
||||
excludedPublicKeys: Array<HexKey>,
|
||||
limit: Int = 21, createdAt: Instant = GENESIS_AT
|
||||
): List<Profile>
|
||||
|
||||
|
||||
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit")
|
||||
fun filterProfiles(searchTerm: String, limit: Int = 21): List<press.mantra.compose.database.model.intermdiate.LocalProfile>
|
||||
suspend fun filterProfiles(searchTerm: String, limit: Int = 21): List<press.mantra.compose.database.model.intermdiate.LocalProfile>
|
||||
|
||||
@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<Profile?>
|
||||
|
||||
@Query("SELECT * FROM Profile WHERE publicKey IN (:publicKeys)")
|
||||
fun getProfileByPublicKeys(publicKeys: List<String>): List<Profile>
|
||||
suspend fun getProfileByPublicKeys(publicKeys: List<String>): List<Profile>
|
||||
|
||||
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey")
|
||||
fun observeProfileWithFollowersByPublicKey(publicKey: String): Flow<press.mantra.compose.database.model.intermdiate.LocalProfileWithFollowers?>
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface ReactionDao {
|
||||
@Query("SELECT * FROM Reaction")
|
||||
fun getAllReactions(): List<press.mantra.compose.database.model.Reaction>
|
||||
suspend fun getAllReactions(): List<press.mantra.compose.database.model.Reaction>
|
||||
|
||||
@Query("SELECT * FROM Reaction WHERE id = :id")
|
||||
fun getReactionById(id: String): Flow<press.mantra.compose.database.model.Reaction?>
|
||||
|
||||
+2
-2
@@ -12,10 +12,10 @@ interface RecentSearchDao {
|
||||
fun observeRecentSearchByQuery(query: String): Flow<press.mantra.compose.database.model.RecentSearch?>
|
||||
|
||||
@Query("SELECT * FROM RecentSearch ORDER BY createdAt DESC LIMIT :limit ")
|
||||
fun getAllRecentSearches(limit: Int = 21): List<press.mantra.compose.database.model.RecentSearch>
|
||||
suspend fun getAllRecentSearches(limit: Int = 21): List<press.mantra.compose.database.model.RecentSearch>
|
||||
|
||||
@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)
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface RelayDao {
|
||||
@Query("SELECT * FROM Relay")
|
||||
fun getAllPosts(): List<press.mantra.compose.database.model.Relay>
|
||||
suspend fun getAllPosts(): List<press.mantra.compose.database.model.Relay>
|
||||
|
||||
@Query("SELECT * FROM Relay WHERE publicKey = :publicKey")
|
||||
fun observePublicKeyRelays(publicKey: String): Flow<List<press.mantra.compose.database.model.Relay>>
|
||||
|
||||
+3
-3
@@ -7,13 +7,13 @@ import androidx.room3.Upsert
|
||||
@Dao
|
||||
interface RepostedRelationDao {
|
||||
@Query("SELECT * FROM RepostedRelation")
|
||||
fun getAllReposts(): List<press.mantra.compose.database.model.RepostedRelation>
|
||||
suspend fun getAllReposts(): List<press.mantra.compose.database.model.RepostedRelation>
|
||||
|
||||
@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)
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ interface SynchronizeNostrEventRequestDao {
|
||||
fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String, status: List<String>): Flow<Int>
|
||||
|
||||
@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<press.mantra.compose.database.model.SynchronizeNostrEventRequest>)
|
||||
suspend fun insert(synchronizeNostrEventRequests: List<press.mantra.compose.database.model.SynchronizeNostrEventRequest>)
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
+3
-3
@@ -12,10 +12,10 @@ import press.mantra.compose.database.model.UnsignedNostrEvent
|
||||
@Dao
|
||||
interface UnsignedNostrEventDao {
|
||||
@Query("SELECT * FROM UnsignedNostrEvent")
|
||||
fun getAUnsignedNostrEvents(): List<UnsignedNostrEvent>
|
||||
suspend fun getAUnsignedNostrEvents(): List<UnsignedNostrEvent>
|
||||
|
||||
@Query("SELECT * FROM UnsignedNostrEvent") // TODO: where nostrEvent.unsignedNostrEventId is 0
|
||||
fun getAllUnprocessedUnsignedNostrEvents(): List<UnsignedNostrEvent>
|
||||
suspend fun getAllUnprocessedUnsignedNostrEvents(): List<UnsignedNostrEvent>
|
||||
|
||||
@Query("SELECT * FROM UnsignedNostrEvent WHERE id = :id")
|
||||
fun getUnsignedNostrEventById(id: Long): Flow<UnsignedNostrEvent?>
|
||||
@@ -32,7 +32,7 @@ interface UnsignedNostrEventDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM UnsignedNostrEvent WHERE kind = 0")
|
||||
fun getLocalAccounts(): List<LocalAccount>
|
||||
suspend fun getLocalAccounts(): List<LocalAccount>
|
||||
|
||||
@Query("SELECT * FROM UnsignedNostrEvent WHERE pubKey = :publicKey AND signedAt IS NULL ORDER BY kind ASC")
|
||||
fun observeUnsignedNostrEvents(publicKey: String): Flow<UnsignedNostrEvent?>
|
||||
|
||||
+1
-1
@@ -647,7 +647,7 @@ class DatabaseNostrRepository(
|
||||
.distinctBy { it.id }
|
||||
}
|
||||
|
||||
private fun matchNegentropicNostrEvents(
|
||||
private suspend fun matchNegentropicNostrEvents(
|
||||
synchronizationFilter: SynchronizationFilter,
|
||||
applyLimits: Boolean,
|
||||
): List<NostrEvent> {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
+21
@@ -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<MantraDatabase> {
|
||||
// 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<MantraDatabase>(name = dbFile.absolutePath)
|
||||
}
|
||||
|
||||
actual fun getInMemoryDatabaseBuilder(platform: PlatformContext): RoomDatabase.Builder<MantraDatabase> {
|
||||
return Room.inMemoryDatabaseBuilder()
|
||||
}
|
||||
}
|
||||
@@ -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<String>): 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<Boolean> {
|
||||
return DataStoreManager(phoenixGlobal.ctx, Chain.Mainnet).loadGlobalPrefsForWallet().getShowIntro
|
||||
}
|
||||
|
||||
actual fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs {
|
||||
return DataStoreManager(phoenixGlobal.ctx, Chain.Mainnet).loadGlobalPrefsForWallet()
|
||||
}
|
||||
@@ -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
|
||||
+157
@@ -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<Map<WalletId, UserWalletMetadata>> {
|
||||
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<String>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-4
@@ -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
|
||||
|
||||
+1
-1
Submodule lightning-kmp-app updated: 6434282380...05ce7eb3ff.
Reference in new issue
Block a user