Pull in foundation phoenix seed code.
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NostrEventDetailRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val nostrEventId: String,
|
||||
): Route() {
|
||||
}
|
||||
@@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchResultRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val query: String
|
||||
): Route()
|
||||
@@ -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()
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WriteNewNoteRoute(
|
||||
val activeUserPublicKey: String,
|
||||
val inReplyToEventId: String? = null,
|
||||
val quotedEventId: String? = null,
|
||||
): Route() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user