Pull in foundation phoenix seed code.

This commit is contained in:
Kgothatso Ngako
2026-06-16 20:28:05 +03:00
parent 11272f51b5
commit a825038c06
52 changed files with 1422 additions and 703 deletions

View File

@@ -7,11 +7,13 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import fr.acinq.phoenix.PhoenixGlobal
@Composable
fun AuxApp(
navController: NavHostController,
auxGlobal: AuxGlobal
auxGlobal: AuxGlobal,
phoenixGlobal: PhoenixGlobal
) {
AuxTheme {
Surface(
@@ -19,6 +21,7 @@ fun AuxApp(
) {
AuxNavHost(
auxGlobal = auxGlobal,
phoenixGlobal = phoenixGlobal,
navController = navController
)
}

View File

@@ -20,12 +20,10 @@ import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.exceptions.GiftWrapImpersonationException
import ac.cord.auxiliary.compose.exceptions.GiftWrapSealDecryptionException
import ac.cord.auxiliary.compose.exceptions.GiftWrapUnsealException
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.managers.toHex
import ac.cord.auxiliary.compose.extensions.toHex
import androidx.room3.Dao
import androidx.room3.Transaction
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -34,13 +32,14 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlin.math.log
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import kotlin.time.Clock
import kotlin.time.Instant
@Dao
abstract class NostrDao(
private val database: AuxDatabase
private val database: AuxDatabase,
) {
val logger = Logger.withTag("NostrDao")
@@ -59,7 +58,8 @@ abstract class NostrDao(
open suspend fun publishNostrEvent(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>
relayURLs: List<String>,
walletManager: WalletManager
) {
logger.i("Publish Nostr Event: $nostrEvent ($relayURLs)")
// Update unsignedEvent with signedTime time...
@@ -75,7 +75,8 @@ abstract class NostrDao(
nostrEvent = nostrEvent,
relayURL = relayURLs.first(),
synchronizationRelayURLs = relayURLs,
level = 0
level = 0,
walletManager = walletManager
)
relayURLs.forEach { relayURL ->
@@ -94,7 +95,8 @@ abstract class NostrDao(
nostrEvent: NostrEvent,
relayURL: String,
synchronizationRelayURLs: List<String>,
level: Int
level: Int,
walletManager: WalletManager
) {
val storedNostrEvent = database.nostrEventDao().getNostrEventById(nostrEvent.id)
@@ -132,7 +134,8 @@ abstract class NostrDao(
nostrEvent = nostrEvent,
relayURL = relayURL,
synchronizationRelayURLs = synchronizationRelayURLs,
level = level
level = level,
walletManager = walletManager
)
}
@@ -140,7 +143,8 @@ abstract class NostrDao(
nostrEvent: NostrEvent,
relayURL: String,
synchronizationRelayURLs: List<String>,
level: Int
level: Int,
walletManager: WalletManager
) {
val profilePublicKeysToSync = mutableMapOf<String, MutableSet<String>>()
val eventIdsToSync = mutableMapOf<String, MutableSet<String>>()
@@ -541,217 +545,251 @@ abstract class NostrDao(
// }
// }
SeedManager.activeKeyPair().let { activeKeyPair ->
giftWrapMessage.decryptGiftWrapSeal(
activeKeyPair
).let { giftWrapSeal ->
if (giftWrapSeal == null) {
throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap")
} else {
database.giftWrapSealDao().upsert(
giftWrapSeal
)
walletManager.keyManager.value?.let { keyManager ->
keyManager.nostrPrivateKey()
}
if (walletManager.isLoaded()) {
val nostrKey = walletManager.keyManager.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
val activeKeyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
)
giftWrapSeal.decryptGiftWrapPayload(
activeKeyPair.privKey!!
).let { decryptedGiftWrapPayload ->
if (decryptedGiftWrapPayload == null) {
throw GiftWrapSealDecryptionException("Failed to decrypt sealed ${giftWrapSeal.id} payload")
} else {
if (giftWrapSeal.publicKey.lowercase() != decryptedGiftWrapPayload.publicKey.lowercase()) {
val giftWrapImpersonation = GiftWrapImpersonationException(
"Seal pubkey (${giftWrapSeal.publicKey}) and payload pubkey (${decryptedGiftWrapPayload.publicKey}) needs to be the same for the giftWrap ${nostrEvent.id}"
)
logger.e("Impersonation", giftWrapImpersonation)
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
}
val result = database.giftWrapPayloadDao().upsert(
decryptedGiftWrapPayload
)
giftWrapMessage.decryptGiftWrapSeal(
activeKeyPair
).let { giftWrapSeal ->
if (giftWrapSeal == null) {
throw GiftWrapUnsealException("Failed to unseal ${nostrEvent.id} from the giftWrap")
} else {
database.giftWrapSealDao().upsert(
giftWrapSeal
)
val giftWrapPayload = if (result != -1L) {
decryptedGiftWrapPayload.copy(
id = result
)
giftWrapSeal.decryptGiftWrapPayload(
activeKeyPair.privKey!!
).let { decryptedGiftWrapPayload ->
if (decryptedGiftWrapPayload == null) {
throw GiftWrapSealDecryptionException("Failed to decrypt sealed ${giftWrapSeal.id} payload")
} else {
decryptedGiftWrapPayload
}
val chatRoomId = giftWrapPayload.aggregatedParticipantsPublicKey()
logger.d("ChatRoomId: $chatRoomId")
if (chatRoomId != null) {
// Find or create chatRoom
val localChatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
if (giftWrapSeal.publicKey.lowercase() != decryptedGiftWrapPayload.publicKey.lowercase()) {
val giftWrapImpersonation = GiftWrapImpersonationException(
"Seal pubkey (${giftWrapSeal.publicKey}) and payload pubkey (${decryptedGiftWrapPayload.publicKey}) needs to be the same for the giftWrap ${nostrEvent.id}"
)
logger.e("Impersonation", giftWrapImpersonation)
throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB...
}
val result = database.giftWrapPayloadDao().upsert(
decryptedGiftWrapPayload
)
if (localChatRoom == null) {
val userPublicKey = SeedManager.activeKeyPair().pubKey.toHex()
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(),
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
)
val giftWrapPayload = if (result != -1L) {
decryptedGiftWrapPayload.copy(
id = result
)
// Add participants...
val participants = giftWrapPayload.participantPTags(
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
).map {
Participant(
participantPublicKey = it.pubKey,
chatRoomId = chatRoomId,
relayHint = it.relayHint?.url
)
}
// Sync missing participant profiles...
participants.forEach { participant ->
val giftWrapParticipantProfile = database.profileDao().getProfileByPublicKey(participant.participantPublicKey)
if (giftWrapParticipantProfile == null) {
// Save a placeholder
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = participant.participantPublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
val recommendRelayUrl = giftWrapMessage.receiverRelayHit
if (recommendRelayUrl != null) {
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf()
}
profilePublicKeysToSync[recommendRelayUrl]?.add(participant.participantPublicKey)
} else {
if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] = mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
}
database.participantDao().upsert(
participants
)
participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant ->
val publicKey = participant.participantPublicKey
val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter = SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair("p", listOf(participant.participantPublicKey))
),
limit = 50
)
database.negentropySynchronizeRequestDao().insert(
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
// TODO: Get relayHint form participant...
if (profilePublicKeysToSync.containsKey(relayURL)) {
profilePublicKeysToSync[relayURL] = mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey)
}
}
} else {
giftWrapPayload.parseSubject()?.let { subject ->
database.chatRoomDao().upsert(
localChatRoom.chatRoom.copy(
subject = subject,
updatedAt = giftWrapPayload.createdAt
)
)
}
decryptedGiftWrapPayload
}
val chatMessageId = database.chatMessageDao().upsert(
ChatMessage(
giftWrapPayloadId = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId,
createdAt = giftWrapPayload.createdAt,
content = giftWrapPayload.content,
)
)
val chatRoomId =
giftWrapPayload.aggregatedParticipantsPublicKey()
logger.d("ChatRoomId: $chatRoomId")
val broadcastNostrEventReceiptId = database.broadcastNostrEventReceiptDao().upsert(
BroadcastNostrEventReceipt(
nostrEventId = nostrEvent.id,
isAccepted = true,
isSync = true,
relayURL = relayURL
if (chatRoomId != null) {
// Find or create chatRoom
val localChatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
)
)
database.chatMessageBroadcastNostrEventReceiptRelationDao().upsert(
ChatMessageBroadcastNostrEventReceiptRelation(
broadcastNostrEventReceiptId = broadcastNostrEventReceiptId,
chatMessageId = chatMessageId
if (localChatRoom == null) {
val userPublicKey = activeKeyPair.pubKey.toHex()
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(),
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
)
)
// Add participants...
val participants = giftWrapPayload.participantPTags(
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
).map {
Participant(
participantPublicKey = it.pubKey,
chatRoomId = chatRoomId,
relayHint = it.relayHint?.url
)
}
// Sync missing participant profiles...
participants.forEach { participant ->
val giftWrapParticipantProfile =
database.profileDao()
.getProfileByPublicKey(participant.participantPublicKey)
if (giftWrapParticipantProfile == null) {
// Save a placeholder
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = participant.participantPublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
val recommendRelayUrl =
giftWrapMessage.receiverRelayHit
if (recommendRelayUrl != null) {
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
profilePublicKeysToSync[recommendRelayUrl] =
mutableSetOf()
}
profilePublicKeysToSync[recommendRelayUrl]?.add(
participant.participantPublicKey
)
} else {
if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
}
database.participantDao().upsert(
participants
)
participants.filter { it.participantPublicKey != userPublicKey }
.forEach { participant ->
val publicKey = participant.participantPublicKey
val chatMessageRelayListEvent =
database.nostrEventDao()
.getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay that were sent by us
val synchronizationFilter =
SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
authors = arrayOf(
userPublicKey
),
tags = mapOf(
Pair(
"p",
listOf(participant.participantPublicKey)
)
),
limit = 50
)
database.negentropySynchronizeRequestDao()
.insert(
chatMessageRelayListEvent.relays()
.map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
// TODO: Get relayHint form participant...
if (profilePublicKeysToSync.containsKey(
relayURL
)
) {
profilePublicKeysToSync[relayURL] =
mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
participant.participantPublicKey
)
}
}
} else {
giftWrapPayload.parseSubject()?.let { subject ->
database.chatRoomDao().upsert(
localChatRoom.chatRoom.copy(
subject = subject,
updatedAt = giftWrapPayload.createdAt
)
)
}
}
val chatMessageId = database.chatMessageDao().upsert(
ChatMessage(
giftWrapPayloadId = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId,
createdAt = giftWrapPayload.createdAt,
content = giftWrapPayload.content,
)
)
)
} else {
logger.w("Failed to setup chatRoom for message")
val broadcastNostrEventReceiptId =
database.broadcastNostrEventReceiptDao().upsert(
BroadcastNostrEventReceipt(
nostrEventId = nostrEvent.id,
isAccepted = true,
isSync = true,
relayURL = relayURL
)
)
database.chatMessageBroadcastNostrEventReceiptRelationDao()
.upsert(
ChatMessageBroadcastNostrEventReceiptRelation(
broadcastNostrEventReceiptId = broadcastNostrEventReceiptId,
chatMessageId = chatMessageId
)
)
} else {
logger.w("Failed to setup chatRoom for message")
}
}
}
}

View File

@@ -12,10 +12,10 @@ import ac.cord.auxiliary.compose.database.model.GiftWrapSeal
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Participant
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.managers.SeedManager
import androidx.room3.Dao
import androidx.room3.Transaction
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
@@ -32,13 +32,12 @@ abstract class NostrNip17Dao(
val logger = Logger.withTag(TAG)
@Transaction
open suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? {
open suspend fun getOrCreateChatRoom(activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? {
logger.d("getOrCreateChatRoom: $publicKey")
val activePublicKey = SeedManager.activePublicKey().toHexKey()
val hexKeys = setOf(
activePublicKey,
activeUserPublicKey,
publicKey
)
@@ -55,7 +54,7 @@ abstract class NostrNip17Dao(
// Create new chatRoom
val chatRoom = ChatRoom(
id = chatRoomId,
userPublicKey = activePublicKey,
userPublicKey = activeUserPublicKey,
subject = defaultSubject,
)
database.chatRoomDao().upsert(chatRoom)

View File

@@ -131,6 +131,7 @@ data class Profile(
@Composable
fun RenderAsListItem(
activeUserPublicKey: String,
onNavigateToEvent: (Route) -> Unit
) {
Card(
@@ -141,6 +142,7 @@ data class Profile(
onClick = {
onNavigateToEvent.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
nostrEventId
)
)
@@ -191,7 +193,9 @@ data class Profile(
@Preview
@Composable
fun ProfileRenderAsListItemPreview() {
fun ProfileRenderAsListItemPreview(
activeUserPublicKey: String
) {
val profile = Profile(
publicKey = "pubKey",
displayName = "John Doe",
@@ -204,6 +208,7 @@ fun ProfileRenderAsListItemPreview() {
modifier = Modifier.padding(20.dp)
) {
profile.RenderAsListItem(
activeUserPublicKey = "",
onNavigateToEvent = {}
)
}

View File

@@ -7,18 +7,15 @@ import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.GiftWrapPayload
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.ChatRepository
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
@@ -53,12 +50,14 @@ class DatabaseChatRepository(
override suspend fun getOrCreateChatRoom(
activeUserPublicKey: HexKey,
publicKey: String,
relayHint: String?,
defaultSubject: String?
): LocalChatRoom? = try {
return database.nostrNip17Dao().getOrCreateChatRoom(
publicKey,
activeUserPublicKey = activeUserPublicKey,
publicKey = publicKey,
relayHint = relayHint,
defaultSubject = defaultSubject
)

View File

@@ -16,7 +16,6 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
@@ -57,6 +56,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import fr.acinq.phoenix.managers.WalletManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -389,12 +389,14 @@ class DatabaseNostrRepository(
override suspend fun publishNostrEvent(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>
relayURLs: List<String>,
walletManager: WalletManager
) {
database.nostrDao().publishNostrEvent(
unsignedNostrEvent,
nostrEvent,
relayURLs
relayURLs,
walletManager = walletManager
)
}
@@ -459,7 +461,8 @@ class DatabaseNostrRepository(
override suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager,
) {
storeNostrEventMutex.withLock {
logger.d("saveNostrEvent: $nostrEvent")
@@ -470,7 +473,8 @@ class DatabaseNostrRepository(
),
synchronizationRelayURLs = synchronizationRelayURLs,
relayURL = synchronizeNostrEventRequest.relayURL,
level = synchronizeNostrEventRequest.level
level = synchronizeNostrEventRequest.level,
walletManager = walletManager
)
database.synchronizeNostrEventRequestDao().upsert(
@@ -489,7 +493,8 @@ class DatabaseNostrRepository(
override suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
) {
logger.d("saveNostrEvent: $nostrEvent")
@@ -498,7 +503,8 @@ class DatabaseNostrRepository(
nostrEvent,
relayURL = negentropySynchronizeRequest.relayURL,
synchronizationRelayURLs = synchronizationRelayURLs,
level = negentropySynchronizeRequest.level
level = negentropySynchronizeRequest.level,
walletManager = walletManager
)
database.negentropySynchronizeRequestDao().upsert(

View File

@@ -0,0 +1,71 @@
package ac.cord.auxiliary.compose.extensions
import ac.cord.auxiliary.compose.exceptions.InvalidNostrPrivateKeyException
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.secp256k1.Hex
import io.ktor.utils.io.core.toByteArray
fun String.assureValidNsec() = if (startsWith("nsec")) this else this.hexToNsecHrp()
fun String.assureValidNpub() = if (startsWith("npub")) this else this.hexToNpubHrp()
fun String.assureValidPubKeyHex() = if (startsWith("npub")) this.bech32ToHexOrThrow() else this
fun String.hexToNoteHrp() =
Bech32.encodeBytes(
hrp = "note",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.hexToNpubHrp() =
Bech32.encodeBytes(
hrp = "npub",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.hexToNsecHrp() =
Bech32.encodeBytes(
hrp = "nsec",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.urlToLnUrlHrp() =
Bech32.encodeBytes(
hrp = "lnurl",
data = this.toByteArray(),
encoding = Bech32.Encoding.Bech32,
)
fun String.bech32ToHexOrThrow() = Bech32.decodeBytes(bech32 = this).second.toHex()
fun String.bech32ToHexOrNull() = runCatching { this.bech32ToHexOrThrow() }.getOrNull()
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
@OptIn(ExperimentalStdlibApi::class)
fun ByteArray.toHex() = Hex.encode(this)
@Throws(IllegalArgumentException::class)
fun String.bechToBytesOrThrow(hrp: String? = null): ByteArray {
val decodedForm = Bech32.decodeBytes(this)
hrp?.also { require(it == decodedForm.first) }
return decodedForm.second
}
fun String.extractKeyPairFromPrivateKeyOrThrow(): Pair<String, String> {
return try {
val nsec = this.assureValidNsec()
val decoded = Bech32.decodeBytes(nsec)
val pubkey = PrivateKey(decoded.second).publicKey().value.toByteArray()
nsec to pubkey.toNpub()
} catch (error: IllegalArgumentException) {
Logger.withTag("String.extractKeyPairFromPrivateKeyOrThrow").w(error) { error.message ?: "" }
throw InvalidNostrPrivateKeyException()
}
}

View File

@@ -0,0 +1,15 @@
package ac.cord.auxiliary.compose.extensions
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.StartBusinessResult
import fr.acinq.phoenix.utils.preferences.GlobalPrefs
import kotlinx.coroutines.flow.Flow
expect suspend fun platformStartupLogic(words: List<String>): StartBusinessResult
expect fun schedulePlatformLogic(phoenixGlobal: PhoenixGlobal)
expect fun getShowIntroFlow(phoenixGlobal: PhoenixGlobal): Flow<Boolean>
expect fun getGlobalPrefs(phoenixGlobal: PhoenixGlobal): GlobalPrefs

View File

@@ -4,7 +4,7 @@ import ac.cord.auxiliary.compose.AuxGlobal
import ac.cord.auxiliary.compose.database.builder.PlatformDatabaseBuilder
import ac.cord.auxiliary.compose.database.builder.getRoomDatabase
class DatabaseManager(
class AuxDatabaseManager(
auxGlobal: AuxGlobal
) {
val auxDatabase by lazy {

View File

@@ -1,171 +0,0 @@
package ac.cord.auxiliary.compose.managers
import ac.cord.auxiliary.compose.exceptions.InvalidNostrPrivateKeyException
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
import com.vitorpamplona.quartz.nip19Bech32.toNsec
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.lightning.Lightning
import fr.acinq.secp256k1.Hex
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
/***
* TODO: Make this a Singleton
*/
class CredentialsManager(
private val persistence: DataStore<Set<Credential>>,
) {
private val scope = CoroutineScope(Dispatchers.IO)
val credentials = persistence.data
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = runBlocking { persistence.data.first() },
)
private suspend fun addCredential(credential: Credential) = persistence.updateData { it + credential }
suspend fun clearCredentials() = persistence.updateData { emptySet() }
fun isExternalSignerCredential(npub: String) =
checkCredentialType(npub = npub, credentialType = CredentialType.ExternalSigner)
fun isNpubCredential(npub: String) = checkCredentialType(npub = npub, credentialType = CredentialType.PublicKey)
suspend fun getOrCreateInternalSignerCredentials() =
credentials.value.find { it.type == CredentialType.InternalSigner }
?: Lightning.randomKey().let { privateKey ->
Credential(
nsec = privateKey.value.toByteArray().toNsec(),
npub = privateKey.publicKey().value.toByteArray().toNpub(),
type = CredentialType.InternalSigner
)
}
private fun checkCredentialType(npub: String, credentialType: CredentialType) =
credentials.value.find { it.npub == npub }?.type == credentialType
suspend fun saveNsec(nostrKey: String): String {
val (nsec, pubkey) = nostrKey.extractKeyPairFromPrivateKeyOrThrow()
addCredential(Credential(nsec = nsec, npub = pubkey, type = CredentialType.PrivateKey))
return pubkey.bech32ToHexOrThrow()
}
suspend fun saveExternalSignerNpub(npub: String): String {
val (hexKey, bech32Key) = if (npub.startsWith("npub")) {
npub.bech32ToHexOrThrow() to npub
} else {
npub to npub.hexToNpubHrp()
}
addCredential(Credential(nsec = null, npub = bech32Key, type = CredentialType.ExternalSigner))
return hexKey
}
suspend fun saveNpub(npub: String): String {
addCredential(Credential(nsec = null, npub = npub, type = CredentialType.PublicKey))
return npub.bech32ToHexOrThrow()
}
suspend fun removeCredentialByNsec(nsec: String) =
persistence.updateData {
it.filterNot { cred -> cred.nsec == nsec }.toSet()
}
suspend fun removeCredentialByNpub(npub: String) =
persistence.updateData {
it.filterNot { cred -> cred.npub == npub }.toSet()
}
fun findOrThrow(npub: String): Credential =
credentials.value.find { it.npub == npub }
?: throw IllegalArgumentException("Credential not found for $npub.")
@Serializable
data class Credential(
val nsec: String?,
val npub: String,
val type: CredentialType = CredentialType.PrivateKey,
)
enum class CredentialType {
InternalSigner,
ExternalSigner,
PrivateKey,
PublicKey,
}
}
fun String.assureValidNsec() = if (startsWith("nsec")) this else this.hexToNsecHrp()
fun String.assureValidNpub() = if (startsWith("npub")) this else this.hexToNpubHrp()
fun String.assureValidPubKeyHex() = if (startsWith("npub")) this.bech32ToHexOrThrow() else this
fun String.hexToNoteHrp() =
Bech32.encodeBytes(
hrp = "note",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.hexToNpubHrp() =
Bech32.encodeBytes(
hrp = "npub",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.hexToNsecHrp() =
Bech32.encodeBytes(
hrp = "nsec",
data = Hex.decode(this),
encoding = Bech32.Encoding.Bech32,
)
fun String.urlToLnUrlHrp() =
Bech32.encodeBytes(
hrp = "lnurl",
data = this.toByteArray(),
encoding = Bech32.Encoding.Bech32,
)
fun String.bech32ToHexOrThrow() = Bech32.decodeBytes(bech32 = this).second.toHex()
fun String.bech32ToHexOrNull() = runCatching { this.bech32ToHexOrThrow() }.getOrNull()
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
@OptIn(ExperimentalStdlibApi::class)
fun ByteArray.toHex() = Hex.encode(this)
@Throws(IllegalArgumentException::class)
fun String.bechToBytesOrThrow(hrp: String? = null): ByteArray {
val decodedForm = Bech32.decodeBytes(this)
hrp?.also { require(it == decodedForm.first) }
return decodedForm.second
}
fun String.extractKeyPairFromPrivateKeyOrThrow(): Pair<String, String> {
return try {
val nsec = this.assureValidNsec()
val decoded = Bech32.decodeBytes(nsec)
val pubkey = PrivateKey(decoded.second).publicKey().value.toByteArray()
nsec to pubkey.toNpub()
} catch (error: IllegalArgumentException) {
Logger.withTag("String.extractKeyPairFromPrivateKeyOrThrow").w(error) { error.message ?: "" }
throw InvalidNostrPrivateKeyException()
}
}

View File

@@ -1,28 +0,0 @@
package ac.cord.auxiliary.compose.managers
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import kotlin.random.Random
object SeedManager {
val logger = Logger.withTag("SeedManager")
private val tempDevelopmentKeyPair = KeyPair(
privKey = Random(21_012_256L).nextBytes(32)
)
init {
logger.d("npub: ${tempDevelopmentKeyPair.pubKey.toNpub()}")
}
fun activeKeyPair(): KeyPair {
// TODO: Actually set and get a pair...
return tempDevelopmentKeyPair
}
fun activePublicKey(): ByteArray {
return activeKeyPair().pubKey
}
}

View File

@@ -16,14 +16,17 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ac.cord.auxiliary.compose.exceptions.NostrPublishException
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.managers.toHex
import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import fr.acinq.phoenix.managers.nostrPublicKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
/**
@@ -34,7 +37,8 @@ import kotlinx.coroutines.flow.Flow
class RelaysSocketManager constructor(
private val nostrSocketClientFactory: NostrSocketClientFactory,
private val cachingImportRepository: CachingImportRepository,
private val relayRepository: RelayRepository
private val relayRepository: RelayRepository,
private val walletManager: WalletManager
) {
val logger = Logger.withTag("RelaysSocketManager")
private val scope = CoroutineScope(Dispatchers.IO)
@@ -59,24 +63,17 @@ class RelaysSocketManager constructor(
observeActiveUserId()
}
private val observeRelayJobs = mutableMapOf<String, Job>()
private val observeRelayJobs = mutableMapOf<HexKey, Job>()
private fun observeActiveUserId() =
scope.launch {
SeedManager.activePublicKey().toHex()?.let { publicKey ->
observeRelayJobs[publicKey]?.cancel()
observeRelayJobs[publicKey] = observeRelays(publicKey)
walletManager.keyManager.collectLatest { keyManager ->
keyManager?.nostrPublicKey()?.value?.toHex()?.let { pubkey ->
observeRelayJobs[pubkey]?.cancel()
observeRelayJobs[pubkey] = observeRelays(pubkey)
}
}
// credentialsManager.credentials.collect { credentials ->
// credentials.forEach { credential ->
// credential.npub.bech32ToHexOrNull()?.let { publicKey ->
// observeRelayJobs[publicKey]?.cancel()
//
// observeRelayJobs[publicKey] = observeRelays(publicKey)
// }
// }
// }
}
private fun observeRelays(publicKey: String): Job =

View File

@@ -23,7 +23,11 @@ interface ChatRepository {
suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow<List<LocalChatMessage>>
suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom?
suspend fun getOrCreateChatRoom(
activeUserPublicKey: HexKey,
publicKey: String, relayHint: String?,
defaultSubject: String? = null
): LocalChatRoom?
suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent?
@@ -63,6 +67,7 @@ interface ChatRepository {
}
override suspend fun getOrCreateChatRoom(
activeUserPublicKey: HexKey,
publicKey: String,
relayHint: String?,
defaultSubject: String?

View File

@@ -6,12 +6,13 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.exceptions.SignatureException
import ac.cord.auxiliary.compose.exceptions.SigningKeyNotFoundException
import ac.cord.auxiliary.compose.exceptions.SigningRejectedException
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.network.UserAgent
import ac.cord.auxiliary.compose.network.asClientTag
import ac.cord.auxiliary.compose.network.dto.RelayDTO
import com.vitorpamplona.quartz.nip19Bech32.toNsec
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
@@ -29,6 +30,7 @@ import kotlinx.coroutines.sync.withLock
*/
class NostrNotaryRepository(
private val nostrRepository: NostrRepository,
private val walletManager: WalletManager,
) {
private val scope = CoroutineScope(Dispatchers.Main)
@@ -70,11 +72,9 @@ class NostrNotaryRepository(
setResponse(SignResult.Rejected(SigningRejectedException()))
}
private fun findNsecOrThrow(pubkey: String): String =
private fun findNsecOrThrow(activeUserPublicKey: String): String =
runCatching {
// val npub = Hex.decode(pubkey).toNpub()
// credentialsStore.findOrThrow(npub = npub).nsec
SeedManager.activeKeyPair().privKey?.toNsec()
walletManager.keyManager.value?.nostrPrivateKey()?.value?.toByteArray()?.toNsec()
}.getOrNull() ?: throw SigningKeyNotFoundException()
private fun signNostrEvent(publicKey: String, event: UnsignedNostrEvent): NostrEvent? {

View File

@@ -12,7 +12,6 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
@@ -20,6 +19,7 @@ import ac.cord.auxiliary.compose.database.model.typealiases.SynchronizationFilte
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import fr.acinq.phoenix.managers.WalletManager
import kotlinx.coroutines.flow.Flow
interface NostrRepository {
@@ -66,7 +66,8 @@ interface NostrRepository {
suspend fun publishNostrEvent(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String> = emptyList()
relayURLs: List<String> = emptyList(),
walletManager: WalletManager
)
suspend fun broadcastProcessed(broadcastNostrEventRequest: BroadcastNostrEventRequest, status: String = "processing")
@@ -83,13 +84,15 @@ interface NostrRepository {
suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
)
suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
)
suspend fun queueSynchronizeNostrEvent(
@@ -202,7 +205,8 @@ interface NostrRepository {
override suspend fun publishNostrEvent(
unsignedNostrEvent: UnsignedNostrEvent,
nostrEvent: NostrEvent,
relayURLs: List<String>
relayURLs: List<String>,
walletManager: WalletManager
) {
}
@@ -229,7 +233,8 @@ interface NostrRepository {
override suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
synchronizeNostrEventRequest: SynchronizeNostrEventRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
) {
TODO("Not yet implemented")
}
@@ -237,7 +242,8 @@ interface NostrRepository {
override suspend fun saveNostrEvent(
nostrEvent: NostrEvent,
negentropySynchronizeRequest: NegentropySynchronizeRequest,
synchronizationRelayURLs: List<String>
synchronizationRelayURLs: List<String>,
walletManager: WalletManager
) {
TODO("Not yet implemented")
}

View File

@@ -45,10 +45,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ChatRoomDetailScreen(
activeUserPublicKey: HexKey,
publicKey: String,
relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
@@ -61,7 +63,8 @@ fun ChatRoomDetailScreen(
relayHint = relayHint,
initialChatRoomDetailUIState = initialChatRoomDetailUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository
chatRepository = chatRepository,
activeUserPublicKey = activeUserPublicKey
)
)
@@ -270,6 +273,7 @@ private fun ChatRoomDetailScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
ChatRoomDetailScreen(
activeUserPublicKey = "",
publicKey = "publicKey",
relayHint = null,
initialChatRoomDetailUIState = ChatRoomDetailUIState.Loaded(

View File

@@ -38,6 +38,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import fr.acinq.bitcoin.Chain
import fr.acinq.phoenix.managers.WalletManager
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -45,12 +47,15 @@ fun CreateProfileScreen(
initialCreateProfileUIState: CreateProfileUIState = CreateProfileUIState.Declaration,
onNavigateToSocialPreconditionRoute: () -> Unit,
onNavigateToEndThis: () -> Unit,
nostrRepository: NostrRepository
nostrRepository: NostrRepository,
writeSeed: (List<String>) -> Unit,
walletManager: WalletManager
) {
val createProfileViewModel: CreateProfileViewModel = viewModel (
factory = CreateProfileViewModel.factory(
initialCreateProfileUIState,
nostrRepository
nostrRepository,
walletManager = walletManager
)
)
Scaffold { innerPadding ->
@@ -270,7 +275,7 @@ fun CreateProfileScreen(
} else {
Button(
onClick = {
createProfileViewModel.createAccount()
createProfileViewModel.createAccount(writeSeed)
}
) {
Text(
@@ -361,6 +366,7 @@ fun CreateAccountScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
CreateProfileScreen(
walletManager = WalletManager(Chain.Mainnet),
initialCreateProfileUIState =
// CreateProfileUIState.InputPrompt,
// CreateProfileUIState.Error,
@@ -399,7 +405,9 @@ fun CreateAccountScreenPreview() {
),
onNavigateToSocialPreconditionRoute = {},
onNavigateToEndThis = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY)
writeSeed = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
)
}
}
}

View File

@@ -50,12 +50,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun HomeScreen(
activeUserPublicKey: HexKey,
initialHomeScreenUIState: HomeScreenUIState = HomeScreenUIState.Loading,
onNavigateToEvent: (Route) -> Unit,
onNavigateToDirectMessageDetail: (Route) -> Unit,
@@ -68,6 +70,7 @@ fun HomeScreen(
val homeScreenViewModel: HomeViewModel = viewModel(
factory = HomeViewModel.factory(
activeUserPublicKey = activeUserPublicKey,
initialHomeScreenUIState = initialHomeScreenUIState,
nostrRepository = nostrRepository,
pagerState = rememberPagerState { HomeScreenType.entries.size }
@@ -115,6 +118,7 @@ fun HomeScreen(
onClick = {
onNavigateToEvent.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
nostrEventId = homeScreenUIState.profileWithFollowing.nostrEvent.id
)
)
@@ -205,6 +209,7 @@ fun HomeScreen(
key(feedListViewModel.feedListUIState) {
feedListViewModel.RenderFeed(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}
@@ -262,6 +267,7 @@ private fun HomeScreenPreview() {
modifier = Modifier.padding(20.dp)
) {
HomeScreen(
activeUserPublicKey = "",
initialHomeScreenUIState = HomeScreenUIState.Loaded(
profileWithFollowing = LocalProfileWithFollowing(
nostrEvent = NostrEvent(

View File

@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@Composable
fun NostrEventDetailScreen(
initialNostrEventDetailUIState: NostrEventDetailUIState,
activeUserPublicKey: HexKey,
nostrEventId: HexKey,
nostrRepository: NostrRepository,
onNavigateBack: () -> Unit,
@@ -48,7 +49,8 @@ fun NostrEventDetailScreen(
factory = NostrEventDetailViewModel.factory(
nostrEventId = nostrEventId,
initialNostrEventDetailUIState = initialNostrEventDetailUIState,
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
activeUserPublicKey = activeUserPublicKey
),
)
@@ -67,6 +69,7 @@ fun NostrEventDetailScreen(
when(feedListUIState.localNostrEvent.nostrEvent.kind) {
MetadataEvent.KIND -> {
MetadataEventDetail(
activeUserPublicKey = activeUserPublicKey,
localNostrEvent = feedListUIState.localNostrEvent,
onNavigateBack = onNavigateBack,
onNavigateToEvent = onNavigateToEvent,
@@ -164,6 +167,7 @@ It has survived not only five centuries, but also the leap into electronic types
)
),
nostrEventId = "",
activeUserPublicKey = "",
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
onNavigateBack = {},
onNavigateToEvent = {},

View File

@@ -63,6 +63,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SearchResultScreen(
activeUserPublicKey: String,
searchQuery: String,
nostrRepository: NostrRepository,
searchRepository: SearchRepository,
@@ -265,6 +266,7 @@ fun SearchResultScreen(
onNavigateToEvent = { nostrEventId ->
onNavigateToEvent.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
nostrEventId = nostrEventId
)
)
@@ -315,6 +317,7 @@ private fun SearchResultScreenPreview() {
modifier = Modifier.padding(20.dp)
) {
SearchResultScreen(
activeUserPublicKey = "",
searchQuery = "What happened...",
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY,

View File

@@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SearchScreen(
activeUserPublicKey: String,
initialSearchUIState: SearchUIState,
nostrRepository: NostrRepository,
searchRepository: SearchRepository,
@@ -165,6 +166,7 @@ fun SearchScreen(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
activeUserPublicKey = activeUserPublicKey,
textFieldState.text.toString()
)
)
@@ -180,6 +182,7 @@ fun SearchScreen(
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
activeUserPublicKey = activeUserPublicKey,
textFieldState.text.toString()
)
)
@@ -202,6 +205,7 @@ fun SearchScreen(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
activeUserPublicKey = activeUserPublicKey,
query = "#${textFieldState.text.toString().replace("#","")}"
)
)
@@ -217,6 +221,7 @@ fun SearchScreen(
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
activeUserPublicKey = activeUserPublicKey,
"#${textFieldState.text.toString().replaceFirst("#","")}"
)
)
@@ -251,7 +256,10 @@ fun SearchScreen(
modifier = Modifier
.clickable {
onNavigateToProfile.invoke(
NostrEventDetailRoute(profile.nostrEventId)
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
profile.nostrEventId
)
)
}
.fillMaxWidth(),
@@ -292,6 +300,7 @@ fun SearchScreen(
onClick = {
onNavigateToProfile.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
profile.nostrEventId
)
)
@@ -353,6 +362,7 @@ fun SearchScreen(
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
activeUserPublicKey = activeUserPublicKey,
query = recentSearch.query
)
)
@@ -404,6 +414,7 @@ private fun SearchScreenPreview() {
modifier = Modifier.padding(20.dp)
) {
SearchScreen(
activeUserPublicKey = "",
initialSearchUIState = SearchUIState.Prompt,
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY,

View File

@@ -63,6 +63,7 @@ import kotlin.time.Clock
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
@Composable
fun WriteNewNoteScreen(
activeUserPublicKey: HexKey,
initialWriteNewNoteUIState: WriteNewNoteUIState = WriteNewNoteUIState.Loading,
replyToNostrEventId: HexKey?,
quotedNostrEventId: HexKey?,
@@ -72,6 +73,7 @@ fun WriteNewNoteScreen(
) {
val writeNewNoteViewModel: WriteNewNoteViewModel = viewModel (
factory = WriteNewNoteViewModel.factory(
activeUserPublicKey = activeUserPublicKey,
replyToNostrEventId = replyToNostrEventId,
quotedNostrEventId = quotedNostrEventId,
initialWriteNewNoteUIState,
@@ -417,6 +419,7 @@ fun WriteNewNoteScreen(
Button(
onClick = {
writeNewNoteViewModel.createNewNote(
activeUserPublicKey = activeUserPublicKey,
onNostrEventPublished = onNostrEventPublished,
inReplyToNostrEvent = writeNewNoteUIState.inReplyToNostrEvent,
quotedNostrEvent = writeNewNoteUIState.quotedNostrEvent
@@ -461,6 +464,7 @@ private fun WriteNewNoteScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
WriteNewNoteScreen(
activeUserPublicKey = "",
replyToNostrEventId = null,
quotedNostrEventId = null,
initialWriteNewNoteUIState =

View File

@@ -4,7 +4,7 @@ import ac.cord.auxiliary.compose.AuxGlobal
import ac.cord.auxiliary.compose.database.repository.DatabaseChatRepository
import ac.cord.auxiliary.compose.database.repository.DatabaseNostrRepository
import ac.cord.auxiliary.compose.database.repository.DatabaseSearchRepository
import ac.cord.auxiliary.compose.managers.DatabaseManager
import ac.cord.auxiliary.compose.managers.AuxDatabaseManager
import ac.cord.auxiliary.compose.ui.composable.CreateProfileScreen
import ac.cord.auxiliary.compose.ui.composable.ChatRoomDetailScreen
import ac.cord.auxiliary.compose.ui.composable.HomeScreen
@@ -61,6 +61,7 @@ import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import co.touchlab.kermit.Logger
import fr.acinq.phoenix.PhoenixGlobal
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -71,6 +72,7 @@ import kotlinx.coroutines.launch
@Composable
fun AuxNavHost(
auxGlobal: AuxGlobal,
phoenixGlobal: PhoenixGlobal,
navController: NavHostController
) {
val logger = Logger.withTag("AuxNavHost")
@@ -85,44 +87,45 @@ fun AuxNavHost(
val lifecycleOwner = LocalLifecycleOwner.current
val databaseManager = DatabaseManager(auxGlobal)
val auxDatabaseManager = AuxDatabaseManager(auxGlobal)
val databaseNostrRepository = DatabaseNostrRepository(
database = databaseManager.auxDatabase,
database = auxDatabaseManager.auxDatabase,
applicationIOScope
)
val databaseChatRepository = DatabaseChatRepository(
database = databaseManager.auxDatabase,
database = auxDatabaseManager.auxDatabase,
applicationIOScope
)
val searchRepository = DatabaseSearchRepository(
database = databaseManager.auxDatabase
database = auxDatabaseManager.auxDatabase
)
val navigationViewModel: NavigationViewModel = viewModel (
factory = NavigationViewModel.factory(
initialNavigationUIState = NavigationUIState.Loading,
nostrRepository = databaseNostrRepository,
phoenixGlobal = phoenixGlobal,
scope = applicationIOScope
)
)
val notaryViewModel: NotaryViewModel = viewModel(
factory = NotaryViewModel.factory(
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
scope = applicationIOScope
)
)
// TODO: Produce a notary UI Element...
val synchronizationViewModel: SynchronizationViewModel = viewModel(
factory = SynchronizationViewModel.factory(
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
relayRepository = databaseNostrRepository,
scope = applicationIOScope
)
)
// val notaryViewModel: NotaryViewModel = viewModel(
// factory = NotaryViewModel.factory(
// nostrRepository = databaseNostrRepository,
// chatRepository = databaseChatRepository,
// scope = applicationIOScope
// )
// )
// // TODO: Produce a notary UI Element...
// val synchronizationViewModel: SynchronizationViewModel = viewModel(
// factory = SynchronizationViewModel.factory(
// nostrRepository = databaseNostrRepository,
// chatRepository = databaseChatRepository,
// relayRepository = databaseNostrRepository,
// scope = applicationIOScope
// )
// )
// TODO: Produce a synchronization UI element...
LaunchedEffect(lifecycleOwner) {
@@ -247,26 +250,44 @@ fun AuxNavHost(
)
}
composable<CreateProfileRoute> {
CreateProfileScreen(
onNavigateToSocialPreconditionRoute = {
navController.navigate(
route = SocialPreconditionRoute
)
},
onNavigateToEndThis = {
navController.navigate(
route = BlankRoute
) {
popUpTo(0)
val walletManager = navigationViewModel.activeWalletInUI.value?.business?.walletManager
if (walletManager != null) {
CreateProfileScreen(
onNavigateToSocialPreconditionRoute = {
navController.navigate(
route = SocialPreconditionRoute
)
},
onNavigateToEndThis = {
navController.navigate(
route = BlankRoute
) {
popUpTo(0)
}
},
nostrRepository = databaseNostrRepository,
walletManager = walletManager,
writeSeed = { words ->
navigationViewModel.writeSeed(
words,
isRestoringWallet = false,
onSeedWritten = { walletId ->
}
)
}
},
nostrRepository = databaseNostrRepository
)
)
} else {
ImplementationPendingScreen("Something went wrong")
}
}
composable<WriteNewNoteRoute> { backStackEntry ->
val route = backStackEntry.toRoute<WriteNewNoteRoute>()
WriteNewNoteScreen(
replyToNostrEventId = route.inReplyToEventId,
activeUserPublicKey = route.activeUserPublicKey,
quotedNostrEventId = route.quotedEventId,
onNostrEventPublished = {
applicationMainScope.launch {
@@ -276,6 +297,7 @@ fun AuxNavHost(
onNavigateToNostrEvent = { hexKey ->
navController.navigate(
route = NostrEventDetailRoute(
activeUserPublicKey = route.activeUserPublicKey,
nostrEventId = hexKey
)
)
@@ -337,8 +359,10 @@ fun AuxNavHost(
}
)
}
composable<FeedRoute> {
composable<FeedRoute> { backStackEntry ->
val route = backStackEntry.toRoute<FeedRoute>()
HomeScreen(
activeUserPublicKey = route.activeUserPublicKey,
onNavigateToEvent = { eventRoute ->
navController.navigate(
route = eventRoute
@@ -346,7 +370,9 @@ fun AuxNavHost(
},
onNavigateToWriteNewNote = {
navController.navigate(
route = WriteNewNoteRoute()
route = WriteNewNoteRoute(
activeUserPublicKey = route.activeUserPublicKey
)
)
},
onNavigateToSearch = {
@@ -375,14 +401,18 @@ fun AuxNavHost(
val route = backStackEntry.toRoute<ChatRoomDetailRoute>()
ChatRoomDetailScreen(
activeUserPublicKey = route.activeUserPublicKey,
publicKey = route.directMessageIdentifier,
relayHint = route.relayHint,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository
)
}
composable<SearchRoute> {
composable<SearchRoute> { backStackEntry ->
val route = backStackEntry.toRoute<SearchRoute>()
SearchScreen(
activeUserPublicKey = route.activeUserPublicKey,
initialSearchUIState = SearchUIState.Prompt,
nostrRepository = databaseNostrRepository,
searchRepository = searchRepository,
@@ -402,6 +432,7 @@ fun AuxNavHost(
val route = backStackEntry.toRoute<SearchResultRoute>()
SearchResultScreen(
activeUserPublicKey = route.activeUserPublicKey,
searchQuery = route.query,
nostrRepository = databaseNostrRepository,
searchRepository = searchRepository,
@@ -419,6 +450,7 @@ fun AuxNavHost(
val route = backStackEntry.toRoute<NostrEventDetailRoute>()
NostrEventDetailScreen(
activeUserPublicKey = route.activeUserPublicKey,
initialNostrEventDetailUIState = NostrEventDetailUIState.Loading,
nostrEventId = route.nostrEventId,
nostrRepository = databaseNostrRepository,
@@ -433,6 +465,7 @@ fun AuxNavHost(
onNavigateToWriteAReply = { nostrEventId ->
navController.navigate(
route = WriteNewNoteRoute(
activeUserPublicKey = route.activeUserPublicKey,
inReplyToEventId = nostrEventId
)
)
@@ -445,6 +478,7 @@ fun AuxNavHost(
onNavigateToQuoteNostrEvent = { nostrEventId ->
navController.navigate(
route = WriteNewNoteRoute(
activeUserPublicKey = route.activeUserPublicKey,
quotedEventId = nostrEventId
)
)

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class ChatRoomDetailRoute(
val activeUserPublicKey: String,
val directMessageIdentifier: String, // TODO: have this as a publicKey
val relayHint: String?
): Route() {

View File

@@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object FeedRoute: Route()
data class FeedRoute(
val activeUserPublicKey: String
): Route()

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class NostrEventDetailRoute(
val activeUserPublicKey: String,
val nostrEventId: String,
): Route() {
}

View File

@@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class SearchResultRoute(
val activeUserPublicKey: String,
val query: String
): Route()

View File

@@ -3,4 +3,6 @@ package ac.cord.auxiliary.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object SearchRoute: Route()
data class SearchRoute(
val activeUserPublicKey: String,
): Route()

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class WriteNewNoteRoute(
val activeUserPublicKey: String,
val inReplyToEventId: String? = null,
val quotedEventId: String? = null,
): Route() {

View File

@@ -60,6 +60,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import kotlinx.coroutines.launch
@@ -67,6 +68,7 @@ import kotlinx.coroutines.launch
@Composable
fun MetadataEventDetail(
localNostrEvent: LocalNostrEvent,
activeUserPublicKey: HexKey,
onNavigateBack: () -> Unit,
onNavigateToEvent: (Route) -> Unit,
onNavigateToEditProfile: () -> Unit,
@@ -80,6 +82,7 @@ fun MetadataEventDetail(
val metadataEventDetailViewModel: MetadataEventDetailViewModel = viewModel(
factory = MetadataEventDetailViewModel.factory(
activeUserPublicKey = activeUserPublicKey,
publicKey = localNostrEvent.nostrEvent.pubKey,
nostrRepository = nostrRepository,
pagerState = rememberPagerState { MetadataEventDetailType.entries.size }
@@ -227,6 +230,7 @@ fun MetadataEventDetail(
onClick = {
onNavigateToChatRoom.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = activeUserPublicKey,
directMessageIdentifier = localNostrEvent.nostrEvent.pubKey,
relayHint = localNostrEvent.nostrEvent.relayUrl,
)
@@ -305,6 +309,7 @@ fun MetadataEventDetail(
key(feedListViewModel.feedListUIState) {
feedListViewModel.RenderFeed(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}
@@ -328,6 +333,7 @@ fun MetadataEventDetail(
key(followingListViewModel.followingListUIState) {
followingListViewModel.RenderFeed(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}
@@ -350,6 +356,7 @@ fun MetadataEventDetail(
key(followersListViewModel.followersListUIState) {
followersListViewModel.RenderFeed(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}
@@ -380,6 +387,7 @@ private fun MetadataEventEventDetailPreview() {
modifier = Modifier.padding(20.dp)
) {
MetadataEventDetail(
activeUserPublicKey = "",
localNostrEvent = LocalNostrEvent(
nostrEvent = NostrEvent(
id = "eventId",

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import fr.acinq.bitcoin.Crypto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
class ChatRoomDetailViewModel(
val directMessageId: String,
val activeUserPublicKey: HexKey,
val relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState,
val nostrRepository: NostrRepository,
@@ -56,6 +58,7 @@ class ChatRoomDetailViewModel(
viewModelScope.launch(Dispatchers.IO) {
// We need to get or create a chat with the user on this publicKey...
val localChatRoom = chatRepository.getOrCreateChatRoom(
activeUserPublicKey = activeUserPublicKey,
publicKey = directMessageId,
relayHint = relayHint
)
@@ -76,6 +79,7 @@ class ChatRoomDetailViewModel(
private const val TAG = "ChatRoomDetailViewModel"
fun factory(
activeUserPublicKey: HexKey,
publicKey: String,
relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
@@ -84,6 +88,7 @@ class ChatRoomDetailViewModel(
): ViewModelProvider.Factory = viewModelFactory {
initializer {
ChatRoomDetailViewModel(
activeUserPublicKey = activeUserPublicKey,
directMessageId = publicKey,
relayHint = relayHint,
initialChatRoomDetailUIState = initialChatRoomDetailUIState,

View File

@@ -144,6 +144,7 @@ class ChatRoomListViewModel(
onClick = {
onNavigateToDirectMessageDetail.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = publicKey,
directMessageIdentifier = localChatRoom.chatRoom.id,
relayHint = localChatRoom.localParticipants.firstOrNull { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }?.participant?.relayHint
)

View File

@@ -1,6 +1,5 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.CreateProfileUIState
import androidx.compose.runtime.MutableState
@@ -13,6 +12,18 @@ import ac.cord.auxiliary.compose.ui.view.state.form.CreateProfileFormState
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import fr.acinq.bitcoin.Chain
import fr.acinq.bitcoin.MnemonicCode
import fr.acinq.bitcoin.byteVector
import fr.acinq.lightning.Lightning
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.phoenix.managers.NodeParamsManager
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import fr.acinq.phoenix.managers.nostrPublicKey
import fr.acinq.phoenix.utils.MnemonicLanguage
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
@@ -20,19 +31,22 @@ import kotlinx.coroutines.launch
class CreateProfileViewModel(
val initialCreateProfileUIState: CreateProfileUIState,
val createProfileFormState: CreateProfileFormState = CreateProfileFormState(),
val nostrRepository: NostrRepository
val nostrRepository: NostrRepository,
val walletManager: WalletManager
): ViewModel() {
companion object {
private const val TAG = "CreateAccountViewModel"
fun factory(
initialCreateProfileUIState: CreateProfileUIState,
nostrRepository: NostrRepository
nostrRepository: NostrRepository,
walletManager: WalletManager
): ViewModelProvider.Factory = viewModelFactory {
initializer {
CreateProfileViewModel(
initialCreateProfileUIState,
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
walletManager = walletManager
)
}
}
@@ -64,18 +78,36 @@ class CreateProfileViewModel(
return true
}
public fun createAccount(
writeSeed: (List<String>) -> Unit
// onNavigateToUnsignedProfile: (UnsignedProfileRoute) -> Unit,
// onNavigateToUnannouncedProfile: (UnannouncedProfileRoute) -> Unit
) {
logger.d { "createAccount" }
isActionPending.value = true
viewModelScope.launch(Dispatchers.IO) {
val publicKey = SeedManager.activePublicKey().toHexKey()
viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
logger.e("error when creating new wallet: ", e)
throw e
}) {
// TODO: Generate a new profile...
logger.d("generating new wallet...")
val entropy = Lightning.randomBytes(16)
val mnemonics = MnemonicCode.toMnemonics(
entropy = entropy,
wordlist = MnemonicLanguage.English.wordlist()
)
writeSeed(mnemonics)
val localKeyManager = LocalKeyManager(
seed = MnemonicCode.toSeed(mnemonics, "").byteVector(),
chain = Chain.Mainnet,
remoteSwapInExtendedPublicKey = NodeParamsManager.remoteSwapInXpub
)
nostrRepository.createNewProfile(
publicKey,
localKeyManager.nostrPublicKey().toHex(),
name = createProfileFormState.nameField.textFieldState.text.toString(),
biography = createProfileFormState.biographyField.textFieldState.text.toString()
)

View File

@@ -113,6 +113,7 @@ class FeedListViewModel(
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderFeed(
activeUserPublicKey: String,
onNavigateToEvent: (Route) -> Unit
) {
Column(
@@ -160,6 +161,7 @@ class FeedListViewModel(
onNavigateToEvent = { nostrEventId ->
onNavigateToEvent.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
nostrEventId = nostrEventId
)
)

View File

@@ -105,6 +105,7 @@ class FollowersListViewModel(
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderFeed(
activeUserPublicKey: String,
onNavigateToEvent: (Route) -> Unit
) {
Column(
@@ -149,6 +150,7 @@ class FollowersListViewModel(
key = { profile -> profile.publicKey }
) { follower ->
follower.RenderAsListItem(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}

View File

@@ -104,6 +104,7 @@ class FollowingListViewModel(
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderFeed(
activeUserPublicKey: String,
onNavigateToEvent: (Route) -> Unit
) {
Column(
@@ -148,6 +149,7 @@ class FollowingListViewModel(
key = { profile -> profile.publicKey }
) { profile ->
profile.RenderAsListItem(
activeUserPublicKey = activeUserPublicKey,
onNavigateToEvent = onNavigateToEvent
)
}

View File

@@ -2,7 +2,6 @@ package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.HomeScreenUIState
import androidx.compose.foundation.pager.PagerState
@@ -15,6 +14,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
@@ -38,6 +38,7 @@ enum class HomeScreenType {
}
class HomeViewModel(
val activeUserPublicKey: HexKey,
val initialHomeScreenUIState: HomeScreenUIState,
val nostrRepository: NostrRepository,
val pagerState: PagerState
@@ -56,10 +57,8 @@ class HomeViewModel(
fun observeActiveUserProfileWithFollowing() {
logger.d("observeActiveUserProfileWithFollowing")
viewModelScope.launch(Dispatchers.IO) {
val activePublicKey = SeedManager.activePublicKey().toHexKey()
nostrRepository.observeProfileWithFollowing(
activePublicKey
activeUserPublicKey
).collect { profileWithFollowing ->
homeScreenUIState = if (profileWithFollowing != null) {
HomeScreenUIState.Loaded(
@@ -76,7 +75,6 @@ class HomeViewModel(
homeScreenType: HomeScreenType,
profileWithFollowing: LocalProfileWithFollowing,
): SynchronizationFilter {
val publicKey = SeedManager.activePublicKey().toHexKey()
val twelveHoursAgo = Clock.System.now().minus(12.hours)
val now = Clock.System.now()
@@ -108,7 +106,7 @@ class HomeViewModel(
FileServersEvent.KIND
),
tags = mapOf(
Pair("p", listOf(publicKey))
Pair("p", listOf(activeUserPublicKey))
),
since = twelveHoursAgo,
until = now,
@@ -121,7 +119,7 @@ class HomeViewModel(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(publicKey))
Pair("p", listOf(activeUserPublicKey))
),
limit = 50
)
@@ -133,12 +131,14 @@ class HomeViewModel(
const val TAG = "HomeViewModel"
fun factory(
activeUserPublicKey: HexKey,
initialHomeScreenUIState: HomeScreenUIState,
nostrRepository: NostrRepository,
pagerState: PagerState
): ViewModelProvider.Factory = viewModelFactory {
initializer {
HomeViewModel(
activeUserPublicKey = activeUserPublicKey,
initialHomeScreenUIState = initialHomeScreenUIState,
nostrRepository = nostrRepository,
pagerState = pagerState,

View File

@@ -118,6 +118,7 @@ class InReplyToViewModel(
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderFeed(
activeUserPublicKey: String,
onNavigateToEvent: (Route) -> Unit
) {
Column(
@@ -165,6 +166,7 @@ class InReplyToViewModel(
onNavigateToEvent = { nostrEventId ->
onNavigateToEvent.invoke(
NostrEventDetailRoute(
activeUserPublicKey = activeUserPublicKey,
nostrEventId = nostrEventId
)
)

View File

@@ -3,7 +3,6 @@ package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.MetadataEventDetailUIState
import androidx.compose.foundation.pager.PagerState
@@ -46,7 +45,8 @@ enum class MetadataEventDetailType {
class MetadataEventDetailViewModel(
initialMetadataEventDetailUIState: MetadataEventDetailUIState = MetadataEventDetailUIState.Loading,
val publicKey: HexKey,
val activeUserPublicKey: HexKey,
val eventPublicKey: HexKey,
val nostrRepository: NostrRepository,
val pagerState: PagerState
): ViewModel() {
@@ -65,18 +65,17 @@ class MetadataEventDetailViewModel(
fun observeMetadataEventRelationWithActiveUser() {
logger.d("observeLocalNostrFeed")
viewModelScope.launch(Dispatchers.IO) {
val activePublicKey = SeedManager.activePublicKey().toHexKey()
nostrRepository.observeRelation(
publicKey,
activePublicKey = activePublicKey
eventPublicKey,
activePublicKey = activeUserPublicKey
).collect { relations ->
logger.d("We found relations: $relations")
metadataEventDetailUIState = MetadataEventDetailUIState.Loaded(
isActiveUser = isActiveUser(activePublicKey),
followingActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == publicKey && connection.destinationPublicKey == activePublicKey },
followedByActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == activePublicKey && connection.destinationPublicKey == publicKey }
isActiveUser = isActiveUser(activeUserPublicKey),
followingActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == eventPublicKey && connection.destinationPublicKey == activeUserPublicKey },
followedByActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == activeUserPublicKey && connection.destinationPublicKey == eventPublicKey }
)
if (isActionPending.value) {
@@ -88,14 +87,12 @@ class MetadataEventDetailViewModel(
}
fun isActiveUser(activePublicKey: HexKey): Boolean {
return activePublicKey == publicKey
return activePublicKey == eventPublicKey
}
fun isActiveUser(): Boolean {
val activePublicKey = SeedManager.activePublicKey().toHexKey()
return isActiveUser(
activePublicKey = activePublicKey
activePublicKey = activeUserPublicKey
)
}
@@ -105,7 +102,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Posts -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
TextNoteEvent.KIND,
@@ -117,7 +114,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Replies -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
TextNoteEvent.KIND,
@@ -130,7 +127,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Articles -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
LongTextNoteEvent.KIND,
@@ -141,7 +138,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Followers -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
ContactListEvent.KIND,
@@ -153,7 +150,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Following -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
ContactListEvent.KIND,
@@ -164,7 +161,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Zaps -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
LnZapEvent.KIND,
@@ -176,7 +173,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Photos -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
PictureEvent.KIND,
@@ -187,7 +184,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Shorts -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
VideoShortEvent.KIND,
@@ -198,7 +195,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Videos -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
VideoNormalEvent.KIND,
@@ -209,7 +206,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Bookmarks -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
BookmarkListEvent.KIND,
@@ -220,7 +217,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Reports -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
ReportEvent.KIND,
@@ -231,7 +228,7 @@ class MetadataEventDetailViewModel(
MetadataEventDetailType.Relays -> {
SynchronizationFilter(
authors = arrayOf(
publicKey
eventPublicKey
),
kinds = arrayOf(
RelayFeedsListEvent.KIND,
@@ -244,32 +241,30 @@ class MetadataEventDetailViewModel(
fun follow() {
isActionPending.value = true
val activePublicKey = SeedManager.activePublicKey().toHexKey()
viewModelScope.launch(Dispatchers.IO) {
val contactListEvent = nostrRepository.getNostrEvent(
publicKey = activePublicKey,
publicKey = activeUserPublicKey,
kind = ContactListEvent.KIND
)
logger.d("ContactListEvent: $contactListEvent")
val followUsers: TagArray = if (contactListEvent != null) {
if (contactListEvent.tags.isTaggedUser(publicKey)) {
if (contactListEvent.tags.isTaggedUser(eventPublicKey)) {
// User already being followed
logger.d("User is already following $publicKey")
logger.d("User is already following $eventPublicKey")
return@launch
}
contactListEvent.tags.plus(
ContactTag(
pubKey = publicKey
pubKey = eventPublicKey
).toTagArray()
)
} else {
arrayOf(
ContactTag(activePublicKey, null, null).toTagArray(),
ContactTag(publicKey, null, null).toTagArray(),
ContactTag(activeUserPublicKey, null, null).toTagArray(),
ContactTag(eventPublicKey, null, null).toTagArray(),
)
}
val contactListTagList = listOf(AltTag.assemble(ContactListEvent.ALT)) +
@@ -277,7 +272,7 @@ class MetadataEventDetailViewModel(
nostrRepository.saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = activePublicKey,
pubKey = activeUserPublicKey,
kind = ContactListEvent.KIND,
tags = contactListTagList.toTypedArray(),
content = RelaySet.assemble(
@@ -291,24 +286,22 @@ class MetadataEventDetailViewModel(
fun unfollow(connection: Connection) {
isActionPending.value = true
val activePublicKey = SeedManager.activePublicKey().toHexKey()
viewModelScope.launch(Dispatchers.IO) {
val contactListEvent = nostrRepository.getNostrEvent(
publicKey = activePublicKey,
publicKey = activeUserPublicKey,
kind = ContactListEvent.KIND
)
if (contactListEvent != null) {
if (!contactListEvent.tags.isTaggedUser(publicKey)) {
logger.d("User is not being followed so no need to unfollow: $publicKey")
if (!contactListEvent.tags.isTaggedUser(eventPublicKey)) {
logger.d("User is not being followed so no need to unfollow: $eventPublicKey")
} else {
nostrRepository.saveUnsignedNostrEvent(
UnsignedNostrEvent(
pubKey = activePublicKey,
pubKey = activeUserPublicKey,
kind = ContactListEvent.KIND,
tags = contactListEvent.tags.filter { it.size > 1 && it[1] != publicKey }.toTypedArray(),
tags = contactListEvent.tags.filter { it.size > 1 && it[1] != eventPublicKey }.toTypedArray(),
content = RelaySet.assemble(
emptyMap()
)
@@ -326,15 +319,17 @@ class MetadataEventDetailViewModel(
const val TAG = "MetadataEventDetailViewModel"
fun factory(
activeUserPublicKey: HexKey,
publicKey: HexKey,
nostrRepository: NostrRepository,
pagerState: PagerState
): ViewModelProvider.Factory = viewModelFactory {
initializer {
MetadataEventDetailViewModel(
activeUserPublicKey = activeUserPublicKey,
nostrRepository = nostrRepository,
pagerState = pagerState,
publicKey = publicKey
eventPublicKey = publicKey,
)
}
}

View File

@@ -1,26 +1,94 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.extensions.getGlobalPrefs
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.widgets.wallet.WalletAvatars
import ac.cord.auxiliary.compose.ui.view.state.NavigationUIState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import fr.acinq.lightning.logging.error
import fr.acinq.phoenix.PhoenixBusiness
import fr.acinq.phoenix.PhoenixGlobal
import fr.acinq.phoenix.data.ActiveWallet
import fr.acinq.phoenix.data.DecryptSeedResult
import fr.acinq.phoenix.data.ElectrumConfig
import fr.acinq.phoenix.data.ListWalletState
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.managers.DataStoreManager
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import fr.acinq.phoenix.managers.nostrPublicKey
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
sealed class WritingSeedState {
data object Init : WritingSeedState()
data class Writing(val mnemonics: List<String>) : WritingSeedState()
data class WrittenToDisk(val walletId: WalletId) : WritingSeedState()
sealed class Error : WritingSeedState() {
data class Generic(val cause: Throwable) : Error()
data object CannotLoadSeedMap: Error()
data object SeedAlreadyExists: Error()
}
}
expect fun updateBusinessActiveInUI(walletId: WalletId)
expect fun loadAndDecryptSeed(phoenixGlobal: PhoenixGlobal): DecryptSeedResult
expect fun getAvailableWalletsMeta(phoenixGlobal: PhoenixGlobal): Flow<Map<WalletId, UserWalletMetadata>>
expect suspend fun saveAvailableWalletMeta(phoenixGlobal: PhoenixGlobal, metadata: UserWalletMetadata)
expect suspend fun saveAvailableWalletMeta(
phoenixGlobal: PhoenixGlobal,
walletId: WalletId,
name: String?,
avatar: String,
isHidden: Boolean
)
expect 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
)
class NavigationViewModel(
initialNavigationUIState: NavigationUIState,
val phoenixGlobal: PhoenixGlobal,
val nostrRepository: NostrRepository,
val scope: CoroutineScope,
): ViewModel() {
@@ -31,10 +99,12 @@ class NavigationViewModel(
fun factory(
initialNavigationUIState: NavigationUIState,
nostrRepository: NostrRepository,
phoenixGlobal: PhoenixGlobal,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
NavigationViewModel(
phoenixGlobal = phoenixGlobal,
initialNavigationUIState = initialNavigationUIState,
nostrRepository = nostrRepository,
scope = scope
@@ -50,72 +120,262 @@ class NavigationViewModel(
)
val navigationUIState = _navigationUIState.asStateFlow()
val listWalletState = mutableStateOf<ListWalletState>(ListWalletState.Init)
private val _availableWallets = MutableStateFlow<Map<WalletId, UserWallet>>(emptyMap())
val availableWallets = _availableWallets.asStateFlow()
private val _desiredWalletId = MutableStateFlow<WalletId?>(null)
val desiredWalletId = _desiredWalletId.asStateFlow()
val startWalletImmediately = MutableStateFlow(true)
private val _activeWalletInUI = MutableStateFlow<ActiveWallet?>(null)
val activeWalletInUI = _activeWalletInUI.asStateFlow()
init {
observeProfile()
}
private fun observeProfile() {
logger.i("observeProfile")
scope.launch(Dispatchers.IO) {
delay(2_100) // Looking busy...
logger.i("Navigation UI State is Landing")
val publicKey = SeedManager.activePublicKey().toHexKey()
logger.i("Observing: $publicKey")
nostrRepository.observeProfile(
publicKey = publicKey
).distinctUntilChanged().collect { localProfile ->
logger.i("Local Profile: $localProfile")
_navigationUIState.getAndUpdate {
if (localProfile == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.unsignedNostrEvent == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.broadcastNostrEventReceipt != null) {
logger.i("We have a broadcast receipt: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.profile != null) {
logger.i("We have successfully synced a profile: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.broadcastNostrEventRequest != null) {
logger.i("We have a broadcast request: ${localProfile.broadcastNostrEventRequest}")
NavigationUIState.UnannouncedProfile(
broadcastNostrEventRequest = localProfile.broadcastNostrEventRequest
)
} else if (localProfile.profile != null) {
logger.i("We have a profile that needs to queued for broadcast: ${localProfile.profile}")
NavigationUIState.UnqueuedProfile(
profile = localProfile.profile
)
} else if (localProfile.nostrEvent != null) {
logger.i("Nostr event with the profile needs to be indexed: ${localProfile.nostrEvent}")
NavigationUIState.UnindexedProfile(
nostrEvent = localProfile.nostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should request to sync profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnqueuedProfileSynchronization(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should be syncing the profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsyncedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
activeWalletInUI.collectLatest { activeWallet ->
if (activeWallet == null) {
// TODO: No active wallet
} else {
if (activeWallet.business != null) {
activeWallet.business.walletManager.keyManager.collectLatest { keyManager ->
keyManager?.nostrPublicKey()?.let { nostrPublicKey ->
val publicKey = nostrPublicKey.toHex()
logger.i("Observing: $publicKey")
nostrRepository.observeProfile(
publicKey = publicKey
).distinctUntilChanged().collect { localProfile ->
logger.i("Local Profile: $localProfile")
_navigationUIState.getAndUpdate {
if (localProfile == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.unsignedNostrEvent == null) {
logger.i("We need to be on the landing screen so user creates account")
NavigationUIState.Landing
} else if (localProfile.broadcastNostrEventReceipt != null) {
logger.i("We have a broadcast receipt: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.profile != null) {
logger.i("We have successfully synced a profile: ${localProfile.profile}")
NavigationUIState.ProfileLoaded(
publicKey = localProfile.unsignedNostrEvent.pubKey
)
} else if (localProfile.broadcastNostrEventRequest != null) {
logger.i("We have a broadcast request: ${localProfile.broadcastNostrEventRequest}")
NavigationUIState.UnannouncedProfile(
broadcastNostrEventRequest = localProfile.broadcastNostrEventRequest
)
} else if (localProfile.profile != null) {
logger.i("We have a profile that needs to queued for broadcast: ${localProfile.profile}")
NavigationUIState.UnqueuedProfile(
profile = localProfile.profile
)
} else if (localProfile.nostrEvent != null) {
logger.i("Nostr event with the profile needs to be indexed: ${localProfile.nostrEvent}")
NavigationUIState.UnindexedProfile(
nostrEvent = localProfile.nostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should request to sync profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnqueuedProfileSynchronization(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else if (localProfile.unsignedNostrEvent.signedAt != null && localProfile.synchronizeNostrEventRequests.isEmpty()) {
logger.i("We should be syncing the profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsyncedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
} else {
logger.i("We have an unsigned profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsignedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
}
}
}
}
}
} else {
logger.i("We have an unsigned profile: ${localProfile.unsignedNostrEvent}")
NavigationUIState.UnsignedProfile(
unsignedNostrEvent = localProfile.unsignedNostrEvent
)
// TODO: We have an error somewhere...
}
}
}
}
}
fun setActiveWallet(walletId: WalletId, business: PhoenixBusiness) {
val dataStoreManager = DataStoreManager(business)
val userPrefs = dataStoreManager.loadUserPrefsForWallet(walletId = walletId)
val internalPrefs = dataStoreManager.loadInternalPrefsForWallet( walletId = walletId)
_activeWalletInUI.value = ActiveWallet(id = walletId, business = business, userPrefs = userPrefs, internalPrefs = internalPrefs)
updateBusinessActiveInUI(walletId)
// scheduleAutoLock()
}
fun listAvailableWallets(onDone: () -> Unit) {
viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, e ->
// logger.error("error when initialising startup-view: ", e)
listWalletState.value = ListWalletState.Error.Generic(e)
}) {
when (val result =
loadAndDecryptSeed(phoenixGlobal)) {
is DecryptSeedResult.Failure.SerializationError -> {
logger.error {"cannot deserialize seed file: "}
listWalletState.value = ListWalletState.Error.Serialization
}
is DecryptSeedResult.Failure.DecryptionError -> {
logger.e("cannot decrypt seed file: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.GeneralException(result.cause)
}
is DecryptSeedResult.Failure.KeyStoreFailure -> {
logger.e("key store failure: ", throwable = result.cause)
listWalletState.value = ListWalletState.Error.DecryptionError.KeystoreFailure(result.cause)
}
is DecryptSeedResult.Failure.SeedFileUnreadable -> {
logger.e("aborting, unreadable seed file")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedInvalid -> {
logger.e("aborting, seed is invalid")
listWalletState.value = ListWalletState.Error.Generic(null)
}
is DecryptSeedResult.Failure.SeedFileNotFound -> {
listWalletState.value = ListWalletState.Success
_availableWallets.value = emptyMap()
}
is DecryptSeedResult.Success -> {
val metadataMap = getAvailableWalletsMeta(
phoenixGlobal
).first()
result.userWalletsMap.forEach { (walletId, _) ->
if (metadataMap[walletId] == null) {
saveAvailableWalletMeta(
phoenixGlobal = phoenixGlobal,
walletId = walletId,
name = null,
avatar = WalletAvatars.list.random(),
isHidden = false
)
}
}
_availableWallets.value = result.userWalletsMap
listWalletState.value = ListWalletState.Success
viewModelScope.launch(Dispatchers.Main) {
onDone()
}
}
}
}
}
// fun scheduleAutoLock() {
// // TODO: Run this through an expect...
// viewModelScope.launch {
// autoLockHandler.removeCallbacksAndMessages(null)
// val activeUserPrefs = activeWalletInUI.first()?.userPrefs ?: return@launch
//
// val biometricLockEnabled = activeUserPrefs.getLockBiometricsEnabled.first()
// val customPinLockEnabled = activeUserPrefs.getLockPinEnabled.first()
// val autoLockDelay = activeUserPrefs.getAutoLockDelay.first()
//
// if ((biometricLockEnabled || customPinLockEnabled) && autoLockDelay != Duration.INFINITE) {
// autoLockHandler.postDelayed(autoLockRunnable, autoLockDelay.inWholeMilliseconds)
// }
// }
// }
/** Clears the active wallet and signals the startup screen to load the given [walletId]. */
fun switchToWallet(walletId: WalletId) {
_desiredWalletId.value = walletId
_activeWalletInUI.value = null
}
/** Clears the active wallet. It does not affect [desiredWalletId]. The UI may still auto-open a specific wallet, if [desiredWalletId] is not null. */
fun clearActiveWallet() {
_activeWalletInUI.value = null
}
/** Resets the active wallet and [desiredWalletId]. The UI will redirect to the startup screen with the wallets selector prompt. */
fun resetToSelector() {
_desiredWalletId.value = null
_activeWalletInUI.value = null
startWalletImmediately.value = false
}
override fun onCleared() {
super.onCleared()
logger.i("AppViewModel cleared")
}
fun getPhoenixGlobalPrefs(): GlobalPrefs {
return getGlobalPrefs(
phoenixGlobal
)
}
// wallet initialisation options -- to be saved to user prefs once the wallet has been created and we know its walletId
var isTorEnabled = mutableStateOf(false)
var customElectrumServer = mutableStateOf<ElectrumConfig.Custom?>(null)
/** Monitors the writing of a seed on disk ; used by the restore view and the create view thru [writeSeed]. */
var writingState by mutableStateOf<WritingSeedState>(
WritingSeedState.Init
)
private set
/**
* Attempts to write a seed on disk and updates the view model state. If a seed already
* exists on disk, this method will put the [writingState] in error.
*/
fun writeSeed(
mnemonics: List<String>,
isRestoringWallet: Boolean,
onSeedWritten: (WalletId) -> Unit
) {
platformWriteSeed(
log = logger,
phoenixGlobal = phoenixGlobal,
globalPrefs = getPhoenixGlobalPrefs(),
writingState = writingState,
viewModelScope = viewModelScope,
mnemonics = mnemonics,
onWritingSeedError = { writingSeedStateError ->
writingState = writingSeedStateError
},
onWritingSeedStateWriting = { writing ->
writingState = writing
},
isRestoringWallet = isRestoringWallet,
isTorEnabled = isTorEnabled.value,
customElectrumServer = customElectrumServer.value,
onSeedWritten = { walletId ->
onSeedWritten(walletId)
}
)
}
}

View File

@@ -2,7 +2,6 @@ package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.NostrEventDetailUIState
@@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NostrEventDetailViewModel(
val activeUserPublicKey: HexKey,
val nostrEventId: HexKey,
initialNostrEventDetailUIState: NostrEventDetailUIState,
val nostrRepository: NostrRepository
@@ -54,7 +54,12 @@ class NostrEventDetailViewModel(
LnZapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(SeedManager.activePublicKey().toHexKey()))
Pair(
"p",
listOf(
activeUserPublicKey
)
)
),
limit = 50
)
@@ -85,6 +90,7 @@ class NostrEventDetailViewModel(
fun factory(
nostrEventId: HexKey,
activeUserPublicKey: HexKey,
initialNostrEventDetailUIState: NostrEventDetailUIState = NostrEventDetailUIState.Loading,
nostrRepository: NostrRepository,
): ViewModelProvider.Factory = viewModelFactory {
@@ -92,7 +98,8 @@ class NostrEventDetailViewModel(
NostrEventDetailViewModel(
nostrEventId = nostrEventId,
initialNostrEventDetailUIState = initialNostrEventDetailUIState,
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
activeUserPublicKey = activeUserPublicKey
)
}
}

View File

@@ -1,65 +1,33 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.GiftWrapSeal
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.network.dto.toRelayDTO
import ac.cord.auxiliary.compose.network.relays.RelayPool.Companion.PUBLISH_TIMEOUT
import ac.cord.auxiliary.compose.network.relays.RelaysSocketManager
import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage
import ac.cord.auxiliary.compose.network.sockets.NostrSocketClientFactory
import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.CachingImportRepository
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.repository.RelayRepository
import ac.cord.auxiliary.compose.ui.view.state.NavigationUIState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.negentropy.Negentropy
import com.vitorpamplona.negentropy.storage.StorageVector
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
import com.vitorpamplona.quartz.utils.TimeUtils
import fr.acinq.phoenix.managers.WalletManager
import fr.acinq.phoenix.managers.nostrPrivateKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Instant
class NotaryViewModel(
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository,
val scope: CoroutineScope,
val walletManager: WalletManager,
): ViewModel() {
companion object {
@@ -69,13 +37,15 @@ class NotaryViewModel(
fun factory(
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
walletManager: WalletManager,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
NotaryViewModel(
nostrRepository = nostrRepository,
chatRepository = chatRepository,
scope = scope
scope = scope,
walletManager = walletManager
)
}
}
@@ -84,59 +54,71 @@ class NotaryViewModel(
private val logger = Logger.withTag(TAG)
init {
observeUnsignedNostrEvents()
observeUnsealedGiftWrapPayloads()
walletManager.keyManager.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
val keyPair = KeyPair(
privKey = nostrPrivateKey.value.toByteArray()
)
observeUnsignedNostrEvents(keyPair)
observeUnsealedGiftWrapPayloads(keyPair)
}
}
private fun observeUnsignedNostrEvents() {
val tempSigner = NostrSignerSync(
SeedManager.activeKeyPair()
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents" }
nostrRepository.observeUnsignedNostrEvents(
publicKey = SeedManager.activePublicKey().toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
scope.launch(Dispatchers.IO) {
unsignedNostrEventOrNull?.let { unsignedNostrEvent ->
logger.d("Unsigned: ${unsignedNostrEvent.kind}")
val event = tempSigner.signNormal<Event>(
createdAt = unsignedNostrEvent.createdAt.epochSeconds,
kind = unsignedNostrEvent.kind,
tags = unsignedNostrEvent.tags,
content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content
)
logger.d("Signed: ${event.toJson()}")
private fun observeUnsignedNostrEvents(
keyPair: KeyPair
) {
val tempSigner = NostrSignerSync(
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsignedNostrEvents" }
nostrRepository.observeUnsignedNostrEvents(
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { unsignedNostrEventOrNull ->
scope.launch(Dispatchers.IO) {
unsignedNostrEventOrNull?.let { unsignedNostrEvent ->
logger.d("Unsigned: ${unsignedNostrEvent.kind}")
val event = tempSigner.signNormal<Event>(
createdAt = unsignedNostrEvent.createdAt.epochSeconds,
kind = unsignedNostrEvent.kind,
tags = unsignedNostrEvent.tags,
content = unsignedNostrEvent.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: unsignedNostrEvent.content
)
logger.d("Signed: ${event.toJson()}")
nostrRepository.publishNostrEvent(
unsignedNostrEvent,
NostrEvent(
id = event.id,
pubKey = event.pubKey,
kind = event.kind,
tags = event.tags,
content = event.content,
createdAt = Instant.fromEpochSeconds(event.createdAt),
sig = event.sig,
unsignedNostrEventId = unsignedNostrEvent.id
),
relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
nostrRepository.publishNostrEvent(
unsignedNostrEvent,
NostrEvent(
id = event.id,
pubKey = event.pubKey,
kind = event.kind,
tags = event.tags,
content = event.content,
createdAt = Instant.fromEpochSeconds(event.createdAt),
sig = event.sig,
unsignedNostrEventId = unsignedNostrEvent.id
),
relayURLs = Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url },
walletManager = walletManager
)
}
}
}
}
}
}
private fun observeUnsealedGiftWrapPayloads() {
private fun observeUnsealedGiftWrapPayloads(
keyPair: KeyPair
) {
val tempSigner = NostrSignerSync(
SeedManager.activeKeyPair()
keyPair
)
scope.launch(Dispatchers.IO) {
logger.i { "observeUnsealedGiftWrapPayloads" }
chatRepository.observeUnsealedGiftWrapPayloads(
publicKey = SeedManager.activePublicKey().toHexKey()
publicKey = keyPair.pubKey.toHexKey()
).distinctUntilChanged().collect { giftWrapPayloadOrNull ->
scope.launch(Dispatchers.IO) {
giftWrapPayloadOrNull?.let { giftWrapPayload ->
@@ -152,5 +134,4 @@ class NotaryViewModel(
}
}
}

View File

@@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd
import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd
import fr.acinq.phoenix.managers.WalletManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
@@ -40,6 +41,7 @@ class SynchronizationViewModel(
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository,
val relayRepository: RelayRepository,
val walletManager: WalletManager,
val scope: CoroutineScope,
): ViewModel() {
@@ -47,10 +49,11 @@ class SynchronizationViewModel(
nostrSocketClientFactory = NostrSocketClientFactory,
cachingImportRepository = CachingImportRepository.NO_OP_CACHING_IMPORT_REPOSITORY,
relayRepository = relayRepository,
walletManager = walletManager
)
companion object {
private const val TAG = "NavigationViewModel"
private const val TAG = "SynchronizationViewModel"
private val mutex = Mutex()
@@ -58,6 +61,7 @@ class SynchronizationViewModel(
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
relayRepository: RelayRepository,
walletManager: WalletManager,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
@@ -65,6 +69,7 @@ class SynchronizationViewModel(
nostrRepository = nostrRepository,
chatRepository = chatRepository,
relayRepository = relayRepository,
walletManager = walletManager,
scope = scope
)
}
@@ -119,7 +124,9 @@ class SynchronizationViewModel(
nostrRepository.saveNostrEvent(
nostrEvent = it,
synchronizeNostrEventRequest,
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL),
walletManager = walletManager
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
}
@@ -131,7 +138,9 @@ class SynchronizationViewModel(
nostrRepository.saveNostrEvent(
nostrEvent = nostrEvent,
synchronizeNostrEventRequest,
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
synchronizationRelayURLs = listOf(synchronizeNostrEventRequest.relayURL),
walletManager = walletManager
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
}
@@ -228,7 +237,9 @@ class SynchronizationViewModel(
nostrRepository.saveNostrEvent(
nostrEvent = it,
negentropySynchronizeRequest,
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL),
walletManager = walletManager
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
}
@@ -240,7 +251,9 @@ class SynchronizationViewModel(
nostrRepository.saveNostrEvent(
nostrEvent = nostrEvent,
negentropySynchronizeRequest,
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL) // TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
synchronizationRelayURLs = listOf(negentropySynchronizeRequest.relayURL),
walletManager = walletManager
// TODO: + Relays.eventPublishRelaySet.map { normalizedRelayUrl -> normalizedRelayUrl.url }
)
}
}

View File

@@ -3,7 +3,6 @@ package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment
@@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
class WriteNewNoteViewModel(
val activeUserPublicKey: HexKey,
val replyToNostrEventId: HexKey?,
val quotedNostrEventId: HexKey?,
@@ -41,6 +41,7 @@ class WriteNewNoteViewModel(
private const val TAG = "WriteNewNoteViewModel"
fun factory(
activeUserPublicKey: HexKey,
replyToNostrEventId: HexKey?,
quotedNostrEventId: HexKey?,
initialWriteNewNoteUIState: WriteNewNoteUIState,
@@ -48,6 +49,7 @@ class WriteNewNoteViewModel(
): ViewModelProvider.Factory = viewModelFactory {
initializer {
WriteNewNoteViewModel(
activeUserPublicKey = activeUserPublicKey,
replyToNostrEventId = replyToNostrEventId,
quotedNostrEventId = quotedNostrEventId,
initialWriteNewNoteUIState = initialWriteNewNoteUIState,
@@ -120,7 +122,7 @@ class WriteNewNoteViewModel(
).firstOrNull()
}
val activePublicKey = SeedManager.activePublicKey().toHexKey()
val activePublicKey = activeUserPublicKey
// TODO: Get note being replied too...
// TODO: Get users being mentioned...
@@ -170,6 +172,7 @@ class WriteNewNoteViewModel(
}
fun createNewNote(
activeUserPublicKey: HexKey,
onNostrEventPublished: () -> Unit,
inReplyToNostrEvent: LocalNostrEvent? = null,
quotedNostrEvent: LocalNostrEvent? = null,
@@ -183,7 +186,7 @@ class WriteNewNoteViewModel(
).filterIsInstance<ContentSegment.NostrProfileSegment>().map { it.pubkey }
nostrRepository.createNewTextNote(
publicKey = SeedManager.activePublicKey().toHexKey(),
publicKey = activeUserPublicKey,
mentionedPublicKeys = mentionedPublicKeys,
textInput = writeNewNoteFormState.textField.textFieldState.text.toString(),
inReplyToNostrEvent = inReplyToNostrEvent,

View File

@@ -113,7 +113,7 @@ object SeedManager {
* Returns an empty map if the seed file does not exist yet.
* Returns null if there was a problem when loading or decrypting the seed file.
*/
suspend fun loadAndDecryptOrNull(phoenixGlobal: PhoenixGlobal): Map<WalletId, UserWallet>? = when (val res = loadAndDecrypt(phoenixGlobal)) {
fun loadAndDecryptOrNull(phoenixGlobal: PhoenixGlobal): Map<WalletId, UserWallet>? = when (val res = loadAndDecrypt(phoenixGlobal)) {
is DecryptSeedResult.Success -> res.userWalletsMap
is DecryptSeedResult.Failure.SeedFileNotFound -> emptyMap()
is DecryptSeedResult.Failure -> null

View File

@@ -18,7 +18,6 @@ package fr.acinq.phoenix.managers
import fr.acinq.bitcoin.*
import fr.acinq.lightning.crypto.Bip84OnChainKeys
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.crypto.LocalKeyManager
import fr.acinq.lightning.crypto.div
import kotlinx.coroutines.CoroutineScope
@@ -94,6 +93,16 @@ fun LocalKeyManager.cloudKey(): ByteVector32 {
return derivePrivateKey(path).privateKey.value
}
/** Key used to encrypt/decrypt blobs we store in the cloud. */
fun LocalKeyManager.nostrPrivateKey(): PrivateKey {
val path = KeyPath(if (isMainnet()) "m/44'/1237'/0'/0/0" else "m/44'/1237'/1'/0/0")
return derivePrivateKey(path).privateKey
}
fun LocalKeyManager.nostrPublicKey(): PublicKey {
return nostrPrivateKey().publicKey()
}
fun LocalKeyManager.cloudKeyHash(): String {
return Crypto.hash160(cloudKey()).byteVector().toHex()
}