mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-19 19:46:35 +00:00
Massive refactoring to unify internal signer and the Amber signer.
This commit is contained in:
parent
69941002df
commit
df9b764c1d
@ -29,11 +29,7 @@ class ImageUploadTesting {
|
||||
var url: String? = null
|
||||
var error: String? = null
|
||||
|
||||
ImageUploader.account = Account(
|
||||
KeyPair()
|
||||
)
|
||||
|
||||
ImageUploader.uploadImage(
|
||||
ImageUploader(Account(KeyPair())).uploadImage(
|
||||
inputStream,
|
||||
bytes.size.toLong(),
|
||||
"image/gif",
|
||||
|
@ -9,13 +9,23 @@ import android.util.Log
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import com.vitorpamplona.amethyst.service.playback.VideoCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
class Amethyst : Application() {
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
applicationIOScope.cancel()
|
||||
}
|
||||
|
||||
val videoCache: VideoCache by lazy {
|
||||
val newCache = VideoCache()
|
||||
newCache.initFileCache(this)
|
||||
|
@ -29,7 +29,10 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
@ -164,7 +167,7 @@ object LocalPreferences {
|
||||
val accInfo = AccountInfo(
|
||||
npub,
|
||||
account.isWriteable(),
|
||||
account.loginWithExternalSigner
|
||||
account.signer is NostrSignerExternal
|
||||
)
|
||||
updateCurrentAccount(npub)
|
||||
addAccount(accInfo)
|
||||
@ -243,16 +246,13 @@ object LocalPreferences {
|
||||
|
||||
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
|
||||
prefs.edit().apply {
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.loginWithExternalSigner)
|
||||
if (account.loginWithExternalSigner) {
|
||||
putBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, account.signer is NostrSignerExternal)
|
||||
if (account.signer is NostrSignerExternal) {
|
||||
remove(PrefKeys.NOSTR_PRIVKEY)
|
||||
} else {
|
||||
account.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) }
|
||||
}
|
||||
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||
putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels)
|
||||
putStringSet(PrefKeys.FOLLOWING_COMMUNITIES, account.followingCommunities)
|
||||
putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers)
|
||||
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays))
|
||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||
putString(PrefKeys.LANGUAGE_PREFS, Event.mapper.writeValueAsString(account.languagePreferences))
|
||||
@ -261,10 +261,10 @@ object LocalPreferences {
|
||||
putString(PrefKeys.REACTION_CHOICES, Event.mapper.writeValueAsString(account.reactionChoices))
|
||||
putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.name)
|
||||
putString(PrefKeys.DEFAULT_FILE_SERVER, account.defaultFileServer.name)
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
|
||||
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList)
|
||||
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList)
|
||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList.value)
|
||||
putString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, account.defaultDiscoveryFollowList.value)
|
||||
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, Event.mapper.writeValueAsString(account.zapPaymentRequest))
|
||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.mapper.writeValueAsString(account.backupContactList))
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||
@ -382,6 +382,7 @@ object LocalPreferences {
|
||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return@with null
|
||||
val loginWithExternalSigner = getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)
|
||||
val privKey = if (loginWithExternalSigner) null else getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||
|
||||
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
|
||||
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
|
||||
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
||||
@ -472,11 +473,16 @@ object LocalPreferences {
|
||||
mapOf()
|
||||
}
|
||||
|
||||
val keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())
|
||||
val signer = if (loginWithExternalSigner) {
|
||||
NostrSignerExternal(pubKey)
|
||||
} else {
|
||||
NostrSignerInternal(keyPair)
|
||||
}
|
||||
|
||||
return@with Account(
|
||||
keyPair = KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels = followingChannels,
|
||||
followingCommunities = followingCommunities,
|
||||
hiddenUsers = hiddenUsers,
|
||||
keyPair = keyPair,
|
||||
signer = signer,
|
||||
localRelays = localRelays,
|
||||
dontTranslateFrom = dontTranslateFrom,
|
||||
languagePreferences = languagePreferences,
|
||||
@ -485,10 +491,10 @@ object LocalPreferences {
|
||||
reactionChoices = reactionChoices,
|
||||
defaultZapType = defaultZapType,
|
||||
defaultFileServer = defaultFileServer,
|
||||
defaultHomeFollowList = defaultHomeFollowList,
|
||||
defaultStoriesFollowList = defaultStoriesFollowList,
|
||||
defaultNotificationFollowList = defaultNotificationFollowList,
|
||||
defaultDiscoveryFollowList = defaultDiscoveryFollowList,
|
||||
defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList),
|
||||
defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList),
|
||||
defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList),
|
||||
defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList),
|
||||
zapPaymentRequest = zapPaymentRequestServer,
|
||||
hideDeleteRequestDialog = hideDeleteRequestDialog,
|
||||
hideBlockAlertDialog = hideBlockAlertDialog,
|
||||
@ -499,8 +505,7 @@ object LocalPreferences {
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
loginWithExternalSigner = loginWithExternalSigner
|
||||
lastReadPerRoute = lastReadPerRoute
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ package com.vitorpamplona.amethyst
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import coil.Coil
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
@ -27,21 +27,19 @@ import com.vitorpamplona.amethyst.service.NostrThreadDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
class ServiceManager {
|
||||
var shouldPauseService: Boolean = true // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var isStarted: Boolean = false // to not open amber in a loop trying to use auth relays and registering for notifications
|
||||
private var account: Account? = null
|
||||
|
||||
private fun start(account: Account) {
|
||||
this.account = account
|
||||
ExternalSignerUtils.account = account
|
||||
start()
|
||||
}
|
||||
|
||||
@ -55,7 +53,7 @@ class ServiceManager {
|
||||
val myAccount = account
|
||||
|
||||
// Resets Proxy Use
|
||||
HttpClient.start(account)
|
||||
HttpClient.start(account?.proxy)
|
||||
OptOutFromFilters.start(account?.warnAboutPostsWithReports ?: true, account?.filterSpamFromStrangers ?: true)
|
||||
Coil.setImageLoader {
|
||||
Amethyst.instance.imageLoaderBuilder().components {
|
||||
@ -80,7 +78,6 @@ class ServiceManager {
|
||||
NostrChatroomListDataSource.account = myAccount
|
||||
NostrVideoDataSource.account = myAccount
|
||||
NostrDiscoveryDataSource.account = myAccount
|
||||
ImageUploader.account = myAccount
|
||||
|
||||
// Notification Elements
|
||||
NostrHomeDataSource.start()
|
||||
@ -171,10 +168,8 @@ class ServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRestartIfItShould() {
|
||||
if (shouldPauseService) {
|
||||
forceRestart(null, true, true)
|
||||
}
|
||||
fun forceRestart() {
|
||||
forceRestart(null, true, true)
|
||||
}
|
||||
|
||||
fun justStart() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1439,7 +1439,7 @@ object LocalCache {
|
||||
|
||||
val childrenToBeRemoved = mutableListOf<Note>()
|
||||
|
||||
val toBeRemoved = account.hiddenUsers.map { userHex ->
|
||||
val toBeRemoved = account.liveHiddenUsers.value?.hiddenUsers?.map { userHex ->
|
||||
(
|
||||
notes.values.filter {
|
||||
it.event?.pubKey() == userHex
|
||||
@ -1447,7 +1447,7 @@ object LocalCache {
|
||||
it.event?.pubKey() == userHex
|
||||
}
|
||||
).toSet()
|
||||
}.flatten()
|
||||
}?.flatten() ?: emptyList()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
removeFromCache(it)
|
||||
|
@ -39,9 +39,11 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.WrappedEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
@ -152,6 +154,7 @@ open class Note(val idHex: String) {
|
||||
this.replyTo = replyTo
|
||||
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
flowSet?.metadata?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,19 +441,68 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun isZappedBy(user: User, account: Account): Boolean {
|
||||
// Zaps who the requester was the user
|
||||
return zaps.any {
|
||||
it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
|
||||
} || zapPayments.any {
|
||||
val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
|
||||
val response = if (zapResponseEvent != null) {
|
||||
account.decryptZapPaymentResponseEvent(zapResponseEvent)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent?.requestAuthor())
|
||||
private fun recursiveIsPaidByCalculation(
|
||||
account: Account,
|
||||
remainingZapPayments: List<Pair<Note, Note?>>,
|
||||
onWasZappedByAuthor: () -> Unit
|
||||
) {
|
||||
if (remainingZapPayments.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingZapPayments.first()
|
||||
|
||||
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
|
||||
if (zapResponseEvent != null) {
|
||||
account.decryptZapPaymentResponseEvent(zapResponseEvent) { response ->
|
||||
if (response is PayInvoiceSuccessResponse && account.isNIP47Author(zapResponseEvent.requestAuthor())) {
|
||||
onWasZappedByAuthor()
|
||||
} else {
|
||||
recursiveIsPaidByCalculation(
|
||||
account,
|
||||
remainingZapPayments.minus(next),
|
||||
onWasZappedByAuthor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun recursiveIsZappedByCalculation(
|
||||
option: Int?,
|
||||
user: User,
|
||||
account: Account,
|
||||
remainingZapEvents: List<Pair<Note, Note?>>,
|
||||
onWasZappedByAuthor: () -> Unit
|
||||
) {
|
||||
if (remainingZapEvents.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingZapEvents.first()
|
||||
|
||||
if (next.first.author?.pubkeyHex == user.pubkeyHex) {
|
||||
onWasZappedByAuthor()
|
||||
} else {
|
||||
account.decryptZapContentAuthor(next.first) {
|
||||
if (it.pubKey == user.pubkeyHex && (option == null || option == (it as? LnZapEvent)?.zappedPollOption())) {
|
||||
onWasZappedByAuthor()
|
||||
} else {
|
||||
recursiveIsZappedByCalculation(option, user, account, remainingZapEvents.minus(next), onWasZappedByAuthor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isZappedBy(user: User, account: Account, onWasZappedByAuthor: () -> Unit) {
|
||||
recursiveIsZappedByCalculation(null, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||
if (account.userProfile() == user) {
|
||||
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
||||
}
|
||||
}
|
||||
|
||||
fun isZappedBy(option: Int?, user: User, account: Account, onWasZappedByAuthor: () -> Unit) {
|
||||
recursiveIsZappedByCalculation(option, user, account, zaps.toList(), onWasZappedByAuthor)
|
||||
}
|
||||
|
||||
fun getReactionBy(user: User): String? {
|
||||
@ -499,49 +551,67 @@ open class Note(val idHex: String) {
|
||||
zapsAmount = sumOfAmounts
|
||||
}
|
||||
|
||||
fun zappedAmountWithNWCPayments(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
|
||||
if (zapPayments.isEmpty()) return zapsAmount
|
||||
|
||||
var sumOfAmounts = zapsAmount
|
||||
|
||||
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
|
||||
zaps.forEach {
|
||||
(it.value as? LnZapEvent)?.lnInvoice()?.let {
|
||||
invoiceSet.add(it)
|
||||
}
|
||||
private fun recursiveZappedAmountCalculation(
|
||||
invoiceSet: LinkedHashSet<String>,
|
||||
remainingZapPayments: List<Pair<Note, Note?>>,
|
||||
signer: NostrSigner,
|
||||
output: BigDecimal,
|
||||
onReady: (BigDecimal) -> Unit
|
||||
) {
|
||||
if (remainingZapPayments.isEmpty()) {
|
||||
onReady(output)
|
||||
return
|
||||
}
|
||||
|
||||
if (privKey != null && walletServicePubkey != null) {
|
||||
zapPayments.forEach {
|
||||
val noteEvent = (it.value?.event as? LnZapPaymentResponseEvent)?.response(
|
||||
privKey,
|
||||
walletServicePubkey
|
||||
)
|
||||
if (noteEvent is PayInvoiceSuccessResponse) {
|
||||
val invoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(
|
||||
privKey,
|
||||
walletServicePubkey
|
||||
)
|
||||
val next = remainingZapPayments.first()
|
||||
|
||||
(next.second?.event as? LnZapPaymentResponseEvent)?.response(signer) { noteEvent ->
|
||||
if (noteEvent is PayInvoiceSuccessResponse) {
|
||||
(next.first.event as? LnZapPaymentRequestEvent)?.lnInvoice(signer) { invoice ->
|
||||
val amount = try {
|
||||
if (invoice == null) {
|
||||
null
|
||||
} else {
|
||||
LnInvoiceUtil.getAmountInSats(invoice)
|
||||
}
|
||||
LnInvoiceUtil.getAmountInSats(invoice)
|
||||
} catch (e: java.lang.Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (invoice != null && amount != null && !invoiceSet.contains(invoice)) {
|
||||
var newAmount = output
|
||||
|
||||
if (amount != null && !invoiceSet.contains(invoice)) {
|
||||
invoiceSet.add(invoice)
|
||||
sumOfAmounts += amount
|
||||
newAmount += amount
|
||||
}
|
||||
|
||||
recursiveZappedAmountCalculation(
|
||||
invoiceSet,
|
||||
remainingZapPayments.minus(next),
|
||||
signer,
|
||||
newAmount,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sumOfAmounts
|
||||
fun zappedAmountWithNWCPayments(signer: NostrSigner, onReady: (BigDecimal) -> Unit) {
|
||||
if (zapPayments.isEmpty()) {
|
||||
onReady(zapsAmount)
|
||||
}
|
||||
|
||||
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
|
||||
zaps.forEach {
|
||||
(it.value?.event as? LnZapEvent)?.lnInvoice()?.let {
|
||||
invoiceSet.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
recursiveZappedAmountCalculation(
|
||||
invoiceSet,
|
||||
zapPayments.toList(),
|
||||
signer,
|
||||
zapsAmount,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun hasPledgeBy(user: User): Boolean {
|
||||
@ -659,7 +729,32 @@ open class Note(val idHex: String) {
|
||||
lastReactionsDownloadTime = emptyMap()
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
val thisEvent = event ?: return false
|
||||
|
||||
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
|
||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
|
||||
accountChoices.hiddenWords.any {
|
||||
thisEvent.content.contains(it, true)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isSensitive = thisEvent.isSensitive()
|
||||
return isBoostedNoteHidden || isHiddenByWord ||
|
||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
}
|
||||
|
||||
var liveSet: NoteLiveSet? = null
|
||||
var flowSet: NoteFlowSet? = null
|
||||
|
||||
@Synchronized
|
||||
fun createOrDestroyLiveSync(create: Boolean) {
|
||||
@ -688,28 +783,45 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
val thisEvent = event ?: return false
|
||||
|
||||
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
|
||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
|
||||
accountChoices.hiddenWords.any {
|
||||
thisEvent.content.contains(it, true)
|
||||
@Synchronized
|
||||
fun createOrDestroyFlowSync(create: Boolean) {
|
||||
if (create) {
|
||||
if (flowSet == null) {
|
||||
flowSet = NoteFlowSet(this)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
if (flowSet != null && flowSet?.isInUse() == false) {
|
||||
flowSet?.destroy()
|
||||
flowSet = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSensitive = thisEvent.isSensitive()
|
||||
return isBoostedNoteHidden || isHiddenByWord ||
|
||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
fun flow(): NoteFlowSet {
|
||||
if (flowSet == null) {
|
||||
createOrDestroyFlowSync(true)
|
||||
}
|
||||
return flowSet!!
|
||||
}
|
||||
|
||||
fun clearFlow() {
|
||||
if (flowSet != null && flowSet?.isInUse() == false) {
|
||||
createOrDestroyFlowSync(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteFlowSet(u: Note) {
|
||||
// Observers line up here.
|
||||
val metadata = NoteBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
metadata.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@ -800,6 +912,27 @@ class NoteLiveSet(u: Note) {
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteBundledRefresherFlow(val note: Note) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
val stateFlow = MutableStateFlow(NoteState(note))
|
||||
|
||||
fun destroy() {
|
||||
bundler.cancel()
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
checkNotInMainThread()
|
||||
|
||||
bundler.invalidate() {
|
||||
checkNotInMainThread()
|
||||
|
||||
stateFlow.emit(NoteState(note))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NoteBundledRefresherLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
|
@ -409,7 +409,9 @@ class UserLiveSet(u: User) {
|
||||
val relays = innerRelays.map { it }
|
||||
val relayInfo = innerRelayInfo.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
val bookmarks = innerBookmarks.map { it }
|
||||
val bookmarks = innerBookmarks.map {
|
||||
it
|
||||
}
|
||||
val statuses = innerStatuses.map { it }
|
||||
|
||||
val profilePictureChanges = innerMetadata.map {
|
||||
|
@ -1,311 +0,0 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SignerType {
|
||||
SIGN_EVENT,
|
||||
NIP04_ENCRYPT,
|
||||
NIP04_DECRYPT,
|
||||
NIP44_ENCRYPT,
|
||||
NIP44_DECRYPT,
|
||||
GET_PUBLIC_KEY,
|
||||
DECRYPT_ZAP_EVENT
|
||||
}
|
||||
|
||||
object ExternalSignerUtils {
|
||||
val content = LruCache<String, String>(10)
|
||||
var isActivityRunning: Boolean = false
|
||||
val cachedDecryptedContent = mutableMapOf<HexKey, String>()
|
||||
lateinit var account: Account
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var decryptResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var blockListResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun requestRejectedToast() {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.sign_request_rejected),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun default() {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun start(activity: MainActivity) {
|
||||
activityResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
decryptResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
content.put(id, event)
|
||||
cachedDecryptedContent[id] = event
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
|
||||
blockListResultLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
requestRejectedToast()
|
||||
} else {
|
||||
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
|
||||
val id = it.data?.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
cachedDecryptedContent[id] = decryptedContent
|
||||
account.live.invalidateData()
|
||||
}
|
||||
}
|
||||
default()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openSigner(
|
||||
data: String,
|
||||
type: SignerType,
|
||||
intentResult: ActivityResultLauncher<Intent>,
|
||||
pubKey: HexKey,
|
||||
id: String
|
||||
) {
|
||||
try {
|
||||
ServiceManager.shouldPauseService = false
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data"))
|
||||
val signerType = when (type) {
|
||||
SignerType.SIGN_EVENT -> "sign_event"
|
||||
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
|
||||
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
|
||||
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
|
||||
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
|
||||
SignerType.GET_PUBLIC_KEY -> "get_public_key"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
|
||||
}
|
||||
intent.putExtra("type", signerType)
|
||||
intent.putExtra("pubKey", pubKey)
|
||||
intent.putExtra("id", id)
|
||||
if (type !== SignerType.GET_PUBLIC_KEY) {
|
||||
intent.putExtra("current_user", account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
intent.`package` = "com.greenart7c3.nostrsigner"
|
||||
intentResult.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
Amethyst.instance.getString(R.string.error_opening_external_signer),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSigner(event: EventInterface, columnName: String = "signature") {
|
||||
checkNotInMainThread()
|
||||
|
||||
val result = getDataFromResolver(SignerType.SIGN_EVENT, arrayOf(event.toJson(), event.pubKey()), columnName)
|
||||
if (result == null) {
|
||||
ServiceManager.shouldPauseService = false
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.SIGN_EVENT,
|
||||
activityResultLauncher,
|
||||
"",
|
||||
event.id()
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
} else {
|
||||
content.put(event.id(), result)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptBlockList(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result == null) {
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
blockListResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
} else {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataFromResolver(signerType: SignerType, data: Array<out String>, columnName: String = "signature"): String? {
|
||||
val localData = if (signerType !== SignerType.GET_PUBLIC_KEY) {
|
||||
data.toList().plus(account.keyPair.pubKey.toNpub()).toTypedArray()
|
||||
} else {
|
||||
data
|
||||
}
|
||||
|
||||
Amethyst.instance.contentResolver.query(
|
||||
Uri.parse("content://com.greenart7c3.nostrsigner.$signerType"),
|
||||
localData,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
if (it == null) {
|
||||
return null
|
||||
}
|
||||
if (it.moveToFirst()) {
|
||||
val index = it.getColumnIndex(columnName)
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column '$columnName' not found")
|
||||
return null
|
||||
}
|
||||
return it.getString(index)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result == null) {
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptDM(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result == null) {
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
} else {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptBookmark(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) {
|
||||
val result = getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey))
|
||||
if (result == null) {
|
||||
openSigner(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
decryptResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
} else {
|
||||
content.put(id, result)
|
||||
cachedDecryptedContent[id] = result
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(decryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_ENCRYPT) {
|
||||
content.remove(id)
|
||||
cachedDecryptedContent.remove(id)
|
||||
val result = getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey))
|
||||
if (result == null) {
|
||||
isActivityRunning = true
|
||||
openSigner(
|
||||
decryptedContent,
|
||||
signerType,
|
||||
activityResultLauncher,
|
||||
pubKey,
|
||||
id
|
||||
)
|
||||
while (isActivityRunning) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
} else {
|
||||
content.put(id, result)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZapEvent(event: LnZapRequestEvent) {
|
||||
val result = getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey))
|
||||
if (result == null) {
|
||||
openSigner(
|
||||
event.toJson(),
|
||||
SignerType.DECRYPT_ZAP_EVENT,
|
||||
decryptResultLauncher,
|
||||
event.pubKey,
|
||||
event.id
|
||||
)
|
||||
} else {
|
||||
content.put(event.id, result)
|
||||
cachedDecryptedContent[event.id] = result
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
@ -19,8 +18,8 @@ object HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun start(account: Account?) {
|
||||
this.internalProxy = account?.proxy
|
||||
fun start(proxy: Proxy?) {
|
||||
this.internalProxy = proxy
|
||||
}
|
||||
|
||||
fun getHttpClient(): OkHttpClient {
|
||||
|
@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.StatusEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
|
||||
// TODO: Migrate this to a property of AccountVi
|
||||
object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
lateinit var account: Account
|
||||
|
||||
@ -99,7 +100,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(ReportEvent.kind),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -131,7 +132,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
),
|
||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||
limit = 4000,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
|
||||
@ -145,7 +146,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
|
||||
val accountChannel = requestNewChannel { time, relayUrl ->
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList.value, relayUrl, time)
|
||||
} else {
|
||||
hasLoadedTheBasics[account.userProfile()] = true
|
||||
|
||||
@ -158,51 +159,21 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
if (event is GiftWrapEvent) {
|
||||
val privateKey = account.keyPair.privKey
|
||||
if (privateKey != null) {
|
||||
event.cachedGift(privateKey)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
if (LocalCache.getNoteIfExists(event.id) != null) return
|
||||
|
||||
event.cachedGift(account.signer) {
|
||||
this.consume(it, relay)
|
||||
}
|
||||
}
|
||||
|
||||
if (event is SealedGossipEvent) {
|
||||
val privateKey = account.keyPair.privKey
|
||||
if (privateKey != null) {
|
||||
event.cachedGossip(privateKey)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
}
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
if (LocalCache.getNoteIfExists(event.id) != null) return
|
||||
|
||||
// Don't store sealed gossips to avoid rebroadcasting by mistake.
|
||||
event.cachedGossip(account.signer) {
|
||||
LocalCache.justConsume(it, relay)
|
||||
}
|
||||
} else {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
@ -225,11 +196,13 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
LocalCache.getNoteIfExists(noteEvent.id())?.addRelay(relay)
|
||||
|
||||
if (noteEvent is GiftWrapEvent) {
|
||||
val gift = noteEvent.cachedGift(privKey) ?: return
|
||||
markInnerAsSeenOnRelay(gift, privKey, relay)
|
||||
noteEvent.cachedGift(account.signer) { gift ->
|
||||
markInnerAsSeenOnRelay(gift, privKey, relay)
|
||||
}
|
||||
} else if (noteEvent is SealedGossipEvent) {
|
||||
val rumor = noteEvent.cachedGossip(privKey) ?: return
|
||||
markInnerAsSeenOnRelay(rumor, privKey, relay)
|
||||
noteEvent.cachedGossip(account.signer) { rumor ->
|
||||
markInnerAsSeenOnRelay(rumor, privKey, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,10 +235,9 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
if (this::account.isInitialized) {
|
||||
val event = account.createAuthEvent(relay, challenge)
|
||||
if (event != null) {
|
||||
account.createAuthEvent(relay, challenge) {
|
||||
Client.send(
|
||||
event,
|
||||
it,
|
||||
relay.url
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@ -12,14 +13,35 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
override fun start() {
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.IO) {
|
||||
account.liveDiscoveryFollowLists.collect {
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
super.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
fun createLiveStreamFilter(): List<TypedFilter> {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
|
||||
return listOfNotNull(
|
||||
TypedFilter(
|
||||
@ -28,7 +50,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = follows,
|
||||
kinds = listOf(LiveActivitiesChatMessageEvent.kind, LiveActivitiesEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
),
|
||||
follows?.let {
|
||||
@ -38,7 +60,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
tags = mapOf("p" to it),
|
||||
kinds = listOf(LiveActivitiesEvent.kind),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -46,7 +68,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}
|
||||
|
||||
fun createPublicChatFilter(): List<TypedFilter> {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
val followChats = account.selectedChatsFollowList().toList()
|
||||
|
||||
return listOf(
|
||||
@ -56,7 +78,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = follows,
|
||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind, ChannelMessageEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
),
|
||||
TypedFilter(
|
||||
@ -65,14 +87,14 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
ids = followChats,
|
||||
kinds = listOf(ChannelCreateEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)?.toList()
|
||||
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
@ -80,13 +102,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
authors = follows,
|
||||
kinds = listOf(CommunityDefinitionEvent.kind, CommunityPostApprovalEvent.kind),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createLiveStreamTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -100,13 +122,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createLiveStreamGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -120,13 +142,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatsTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -140,13 +162,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createPublicChatsGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -160,13 +182,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -180,13 +202,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createCommunitiesGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList)
|
||||
val hashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -200,13 +222,13 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 300,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val discoveryFeedChannel = requestNewChannel() { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList, relayUrl, time)
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultDiscoveryFollowList.value, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
@ -19,42 +19,35 @@ import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
private val cacheListener: (UserState) -> Unit = {
|
||||
invalidateFilters()
|
||||
}
|
||||
var job: Job? = null
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun start() {
|
||||
if (this::account.isInitialized) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
account.userProfile().live().follows.observeForever(cacheListener)
|
||||
job?.cancel()
|
||||
job = account.scope.launch(Dispatchers.IO) {
|
||||
account.liveHomeFollowLists.collect {
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
super.start()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
if (this::account.isInitialized) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
account.userProfile().live().follows.removeObserver(cacheListener)
|
||||
}
|
||||
}
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
fun createFollowAccountsFilter(): TypedFilter {
|
||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
|
||||
val follows = account.liveHomeFollowLists.value?.users
|
||||
val followSet = follows?.plus(account.userProfile().pubkeyHex)?.toList()?.ifEmpty { null }
|
||||
|
||||
return TypedFilter(
|
||||
@ -76,13 +69,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
),
|
||||
authors = followSet,
|
||||
limit = 400,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val hashToLoad = account.liveHomeFollowLists.value?.hashtags ?: return null
|
||||
|
||||
if (hashToLoad.isEmpty()) return null
|
||||
|
||||
@ -96,13 +89,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val hashToLoad = account.liveHomeFollowLists.value?.geotags ?: return null
|
||||
|
||||
if (hashToLoad.isEmpty()) return null
|
||||
|
||||
@ -116,13 +109,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowCommunitiesFilter(): TypedFilter? {
|
||||
val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val communitiesToLoad = account.liveHomeFollowLists.value?.communities ?: return null
|
||||
|
||||
if (communitiesToLoad.isEmpty()) return null
|
||||
|
||||
@ -143,13 +136,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
"a" to communitiesToLoad.toList()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val followAccountChannel = requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time)
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList.value, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
|
@ -7,12 +7,13 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
class NostrLnZapPaymentResponseDataSource(
|
||||
private val fromServiceHex: String,
|
||||
private val toUserHex: String,
|
||||
private val replyingToHex: String,
|
||||
private val authSigningKey: ByteArray
|
||||
private val authSigner: NostrSigner
|
||||
) : NostrDataSource("LnZapPaymentResponseFeed") {
|
||||
|
||||
val feedTypes = setOf(FeedType.WALLET_CONNECT)
|
||||
@ -44,10 +45,11 @@ class NostrLnZapPaymentResponseDataSource(
|
||||
override fun auth(relay: Relay, challenge: String) {
|
||||
super.auth(relay, challenge)
|
||||
|
||||
val event = RelayAuthEvent.create(relay.url, challenge, "", authSigningKey)
|
||||
Client.send(
|
||||
event,
|
||||
relay.url
|
||||
)
|
||||
RelayAuthEvent.create(relay.url, challenge, authSigner) {
|
||||
Client.send(
|
||||
it,
|
||||
relay.url
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexValidator
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||
@ -36,7 +37,13 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
}
|
||||
|
||||
val hexToWatch = try {
|
||||
Nip19.uriToRoute(mySearchString)?.hex ?: Hex.decode(mySearchString).toHexKey()
|
||||
val isAStraightHex = if (HexValidator.isHex(mySearchString)) {
|
||||
Hex.decode(mySearchString).toHexKey()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Nip19.uriToRoute(mySearchString)?.hex ?: isAStraightHex
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@ -108,11 +115,14 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
||||
}
|
||||
|
||||
fun search(searchString: String) {
|
||||
println("DataSource: ${this.javaClass.simpleName} Search for $searchString")
|
||||
this.searchString = searchString
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
println("DataSource: ${this.javaClass.simpleName} Clear")
|
||||
searchString = null
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
@ -7,14 +8,35 @@ import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
lateinit var account: Account
|
||||
|
||||
val scope = Amethyst.instance.applicationIOScope
|
||||
val latestEOSEs = EOSEAccount()
|
||||
|
||||
fun createContextualFilter(): TypedFilter? {
|
||||
val follows = account.selectedUsersFollowList(account.defaultStoriesFollowList)?.toList()
|
||||
var job: Job? = null
|
||||
|
||||
override fun start() {
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.IO) {
|
||||
account.liveStoriesFollowLists.collect {
|
||||
invalidateFilters()
|
||||
}
|
||||
}
|
||||
super.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
fun createContextualFilter(): TypedFilter {
|
||||
val follows = account.liveStoriesFollowLists.value?.users?.toList()
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
@ -22,15 +44,15 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
authors = follows,
|
||||
kinds = listOf(FileHeaderEvent.kind, FileStorageHeaderEvent.kind),
|
||||
limit = 200,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowTagsFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedTagsFollowList(account.defaultStoriesFollowList)
|
||||
val hashToLoad = account.liveStoriesFollowLists.value?.hashtags?.toList() ?: return null
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
if (hashToLoad.isEmpty()) return null
|
||||
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
@ -42,13 +64,13 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createFollowGeohashesFilter(): TypedFilter? {
|
||||
val hashToLoad = account.selectedGeohashesFollowList(account.defaultStoriesFollowList)
|
||||
val hashToLoad = account.liveStoriesFollowLists.value?.geotags?.toList() ?: return null
|
||||
|
||||
if (hashToLoad.isNullOrEmpty()) return null
|
||||
|
||||
@ -62,13 +84,13 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
|
||||
}.flatten()
|
||||
),
|
||||
limit = 100,
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList)?.relayList
|
||||
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultStoriesFollowList.value)?.relayList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val videoFeedChannel = requestNewChannel() { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList, relayUrl, time)
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultStoriesFollowList.value, relayUrl, time)
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
|
@ -148,6 +148,23 @@ class ZapPaymentHandler(val account: Account) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareZapRequestIfNeeded(
|
||||
note: Note,
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null,
|
||||
onReady: (String?) -> Unit
|
||||
) {
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
account.createZapRequestFor(note, pollOption, message, zapType, overrideUser) { zapRequest ->
|
||||
onReady(zapRequest.toJson())
|
||||
}
|
||||
} else {
|
||||
onReady(null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerZap(
|
||||
lud16: String,
|
||||
note: Note,
|
||||
@ -161,54 +178,49 @@ class ZapPaymentHandler(val account: Account) {
|
||||
zapType: LnZapEvent.ZapType,
|
||||
overrideUser: User? = null
|
||||
) {
|
||||
var zapRequestJson = ""
|
||||
onProgress(0.05f)
|
||||
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP) {
|
||||
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
|
||||
if (zapRequest != null) {
|
||||
zapRequestJson = zapRequest.toJson()
|
||||
}
|
||||
}
|
||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType, overrideUser) { zapRequestJson ->
|
||||
onProgress(0.10f)
|
||||
|
||||
onProgress(0.10f)
|
||||
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount,
|
||||
message,
|
||||
zapRequestJson,
|
||||
onSuccess = {
|
||||
onProgress(0.7f)
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = it,
|
||||
note,
|
||||
onResponse = { response ->
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
context.getString(
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount,
|
||||
message,
|
||||
zapRequestJson,
|
||||
onSuccess = {
|
||||
onProgress(0.7f)
|
||||
if (account.hasWalletConnectSetup()) {
|
||||
account.sendZapPaymentRequestFor(
|
||||
bolt11 = it,
|
||||
note,
|
||||
onResponse = { response ->
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
context.getString(
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
onProgress(0.8f)
|
||||
} else {
|
||||
onPayInvoiceThroughIntent(it)
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress,
|
||||
context = context
|
||||
)
|
||||
)
|
||||
onProgress(0.8f)
|
||||
} else {
|
||||
onPayInvoiceThroughIntent(it)
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import com.vitorpamplona.quartz.encoders.toLnUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
@ -33,12 +31,12 @@ class LightningAddressResolver() {
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun fetchLightningAddressJson(
|
||||
private fun fetchLightningAddressJson(
|
||||
lnaddress: String,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context
|
||||
) = withContext(Dispatchers.IO) {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val url = assembleUrl(lnaddress)
|
||||
@ -51,7 +49,7 @@ class LightningAddressResolver() {
|
||||
lnaddress
|
||||
)
|
||||
)
|
||||
return@withContext
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
@ -88,15 +86,17 @@ class LightningAddressResolver() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchLightningInvoice(
|
||||
fun fetchLightningInvoice(
|
||||
lnCallback: String,
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context
|
||||
) = withContext(Dispatchers.IO) {
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
@ -124,7 +124,7 @@ class LightningAddressResolver() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
|
||||
fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
|
||||
fetchLightningAddressJson(
|
||||
lnaddress,
|
||||
onSuccess = {
|
||||
@ -135,12 +135,12 @@ class LightningAddressResolver() {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun lnAddressInvoice(
|
||||
fun lnAddressInvoice(
|
||||
lnaddress: String,
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
context: Context
|
||||
|
@ -7,8 +7,6 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.SignerType
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
@ -42,110 +40,40 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
}
|
||||
|
||||
private suspend fun consumeIfMatchesAccount(pushWrappedEvent: GiftWrapEvent, account: Account) {
|
||||
val key = account.keyPair.privKey
|
||||
if (account.loginWithExternalSigner) {
|
||||
ExternalSignerUtils.account = account
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
pushWrappedEvent.content,
|
||||
pushWrappedEvent.pubKey,
|
||||
pushWrappedEvent.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[pushWrappedEvent.id] ?: ""
|
||||
}
|
||||
pushWrappedEvent.unwrap(cached)?.let { notificationEvent ->
|
||||
if (!LocalCache.justVerify(notificationEvent)) return // invalid event
|
||||
if (LocalCache.notes[notificationEvent.id] != null) return // already processed
|
||||
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key != null) {
|
||||
pushWrappedEvent.unwrap(key)?.let { notificationEvent ->
|
||||
LocalCache.justConsume(notificationEvent, null)
|
||||
|
||||
unwrapAndConsume(notificationEvent, account)?.let { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
unwrapAndConsume(notificationEvent, account) { innerEvent ->
|
||||
if (innerEvent is PrivateDmEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is LnZapEvent) {
|
||||
notify(innerEvent, account)
|
||||
} else if (innerEvent is ChatMessageEvent) {
|
||||
notify(innerEvent, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapAndConsume(event: Event, account: Account): Event? {
|
||||
if (!LocalCache.justVerify(event)) return null
|
||||
private fun unwrapAndConsume(event: Event, account: Account, onReady: (Event) -> Unit) {
|
||||
if (!LocalCache.justVerify(event)) return
|
||||
|
||||
return when (event) {
|
||||
when (event) {
|
||||
is GiftWrapEvent -> {
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGift(key)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGift(account.keyPair.pubKey, cached)?.let {
|
||||
unwrapAndConsume(it, account)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
event.cachedGift(account.signer) {
|
||||
unwrapAndConsume(it, account, onReady)
|
||||
}
|
||||
}
|
||||
is SealedGossipEvent -> {
|
||||
val key = account.keyPair.privKey
|
||||
if (key != null) {
|
||||
event.cachedGossip(key)?.let {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else if (account.loginWithExternalSigner) {
|
||||
var cached = ExternalSignerUtils.cachedDecryptedContent[event.id]
|
||||
if (cached == null) {
|
||||
ExternalSignerUtils.decrypt(
|
||||
event.content,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
SignerType.NIP44_DECRYPT
|
||||
)
|
||||
cached = ExternalSignerUtils.cachedDecryptedContent[event.id] ?: ""
|
||||
}
|
||||
event.cachedGossip(account.keyPair.pubKey, cached)?.let {
|
||||
LocalCache.justConsume(it, null)
|
||||
it
|
||||
}
|
||||
} else {
|
||||
null
|
||||
event.cachedGossip(account.signer) {
|
||||
// this is not verifiable
|
||||
LocalCache.justConsume(it, null)
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LocalCache.justConsume(event, null)
|
||||
event
|
||||
onReady(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,11 +128,12 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
|
||||
note.author?.let {
|
||||
if (ChatroomKey(persistentSetOf(it.pubkeyHex)) in knownChatrooms) {
|
||||
val content = acc.decryptContent(note) ?: ""
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
|
||||
acc.decryptContent(note) { content ->
|
||||
val user = note.author?.toBestDisplayName() ?: ""
|
||||
val userPicture = note.author?.profilePicture()
|
||||
val noteUri = note.toNEvent()
|
||||
notificationManager().sendDMNotification(event.id, content, user, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,39 +146,35 @@ class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
if (event.createdAt < TimeUtils.fiveMinutesAgo()) return
|
||||
|
||||
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return
|
||||
|
||||
if ((event.amount ?: BigDecimal.ZERO) < BigDecimal.TEN) return
|
||||
|
||||
if (acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
|
||||
val amount = showAmount(event.amount)
|
||||
val senderInfo = (noteZapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
|
||||
if (decryptedContent != null) {
|
||||
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
Pair(author, decryptedContent.content)
|
||||
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
|
||||
Pair(noteZapRequest.author, noteZapRequest.event?.content())
|
||||
} else {
|
||||
Pair(noteZapRequest.author, null)
|
||||
(noteZapRequest.event as? LnZapRequestEvent)?.let { event ->
|
||||
acc.decryptZapContentAuthor(noteZapRequest) {
|
||||
val author = LocalCache.getOrCreateUser(it.pubKey)
|
||||
val senderInfo = Pair(author, it.content.ifBlank { null })
|
||||
|
||||
acc.decryptContent(noteZapped) {
|
||||
val zappedContent = it.split("\n").get(0)
|
||||
|
||||
val user = senderInfo.first.toBestDisplayName()
|
||||
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo.second?.ifBlank { null }?.let {
|
||||
title += " ($it)"
|
||||
}
|
||||
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
|
||||
zappedContent?.let {
|
||||
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val zappedContent =
|
||||
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0) }
|
||||
|
||||
val user = senderInfo?.first?.toBestDisplayName() ?: ""
|
||||
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||
senderInfo?.second?.ifBlank { null }?.let {
|
||||
title += " ($it)"
|
||||
}
|
||||
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
|
||||
zappedContent?.let {
|
||||
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
|
||||
}
|
||||
val userPicture = senderInfo?.first?.profilePicture()
|
||||
val noteUri = "nostr:Notifications"
|
||||
notificationManager().sendZapNotification(event.id, content, title, userPicture, noteUri, applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import android.util.Log
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -16,24 +16,39 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
class RegisterAccounts(
|
||||
private val accounts: List<AccountInfo>
|
||||
) {
|
||||
private fun recursiveAuthCreation(
|
||||
notificationToken: String,
|
||||
remainingTos: List<Pair<Account, String>>,
|
||||
output: MutableList<RelayAuthEvent>,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit
|
||||
) {
|
||||
if (remainingTos.isEmpty()) {
|
||||
onReady(output)
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingTos.first()
|
||||
|
||||
next.first.createAuthEvent(next.second, notificationToken) {
|
||||
output.add(it)
|
||||
recursiveAuthCreation(notificationToken, remainingTos.filter { next != it }, output, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
// creates proof that it controls all accounts
|
||||
private suspend fun signEventsToProveControlOfAccounts(
|
||||
accounts: List<AccountInfo>,
|
||||
notificationToken: String
|
||||
): List<RelayAuthEvent> {
|
||||
return accounts.mapNotNull {
|
||||
notificationToken: String,
|
||||
onReady: (List<RelayAuthEvent>) -> Unit
|
||||
) {
|
||||
val readyToSend = accounts.mapNotNull {
|
||||
val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)
|
||||
if (acc != null && (acc.isWriteable() || acc.loginWithExternalSigner)) {
|
||||
if (acc.loginWithExternalSigner) {
|
||||
ExternalSignerUtils.account = acc
|
||||
}
|
||||
|
||||
if (acc != null && acc.isWriteable()) {
|
||||
val readRelays = acc.userProfile().latestContactList?.relays() ?: acc.backupContactList?.relays()
|
||||
|
||||
val relayToUse = readRelays?.firstNotNullOfOrNull { if (it.value.read) it.key else null }
|
||||
if (relayToUse != null) {
|
||||
acc.createAuthEvent(relayToUse, notificationToken)
|
||||
Pair(acc, relayToUse)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -41,6 +56,14 @@ class RegisterAccounts(
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val listOfAuthEvents = mutableListOf<RelayAuthEvent>()
|
||||
recursiveAuthCreation(
|
||||
notificationToken,
|
||||
readyToSend,
|
||||
listOfAuthEvents,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||
@ -75,9 +98,10 @@ class RegisterAccounts(
|
||||
}
|
||||
|
||||
suspend fun go(notificationToken: String) = withContext(Dispatchers.IO) {
|
||||
postRegistrationEvent(
|
||||
signEventsToProveControlOfAccounts(accounts, notificationToken)
|
||||
)
|
||||
signEventsToProveControlOfAccounts(accounts, notificationToken) {
|
||||
postRegistrationEvent(it)
|
||||
}
|
||||
|
||||
PushNotificationUtils.hasInit = true
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
|
||||
@ -53,15 +52,15 @@ import java.nio.charset.StandardCharsets
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val isOnMobileDataState = mutableStateOf(false)
|
||||
private val isOnWifiDataState = mutableStateOf(false)
|
||||
|
||||
// Service Manager is only active when the activity is active.
|
||||
private val serviceManager = ServiceManager()
|
||||
val serviceManager = ServiceManager()
|
||||
private var shouldPauseService = true
|
||||
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ExternalSignerUtils.start(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
@ -97,6 +96,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareToLaunchSigner() {
|
||||
shouldPauseService = false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@ -104,8 +107,8 @@ class MainActivity : AppCompatActivity() {
|
||||
// starts muted every time
|
||||
DefaultMutedSetting.value = true
|
||||
|
||||
// Only starts after login
|
||||
if (serviceManager.shouldPauseService) {
|
||||
// Keep connection alive if it's calling the signer app
|
||||
if (shouldPauseService) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
serviceManager.justStart()
|
||||
}
|
||||
@ -116,6 +119,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager).registerDefaultNetworkCallback(networkCallback)
|
||||
|
||||
// resets state until next External Signer Call
|
||||
shouldPauseService = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -128,7 +134,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
// }
|
||||
|
||||
if (serviceManager.shouldPauseService) {
|
||||
if (shouldPauseService) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
serviceManager.pauseForGood()
|
||||
}
|
||||
@ -166,7 +172,7 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onAvailable(network)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
serviceManager.forceRestartIfItShould()
|
||||
serviceManager.forceRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,10 +188,24 @@ class MainActivity : AppCompatActivity() {
|
||||
val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
Log.d("ServiceManager NetworkCallback", "onCapabilitiesChanged: ${network.networkHandle} hasMobileData $isOnMobileData hasWifi $isOnWifi")
|
||||
|
||||
var changedNetwork = false
|
||||
|
||||
if (isOnMobileDataState.value != isOnMobileData) {
|
||||
isOnMobileDataState.value = isOnMobileData
|
||||
|
||||
serviceManager.forceRestartIfItShould()
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (isOnWifiDataState.value != isOnWifi) {
|
||||
isOnWifiDataState.value = isOnWifi
|
||||
|
||||
changedNetwork = true
|
||||
}
|
||||
|
||||
if (changedNetwork) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
serviceManager.forceRestart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@ -26,9 +27,7 @@ import java.util.Base64
|
||||
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
|
||||
|
||||
fun randomChars() = List(16) { charPool.random() }.joinToString("")
|
||||
object ImageUploader {
|
||||
lateinit var account: Account
|
||||
|
||||
class ImageUploader(val account: Account?) {
|
||||
fun uploadImage(
|
||||
uri: Uri,
|
||||
contentType: String?,
|
||||
@ -109,61 +108,73 @@ object ImageUploader {
|
||||
)
|
||||
.build()
|
||||
|
||||
server.clientID(requestBody.toString())?.let {
|
||||
requestBuilder.addHeader("Authorization", it)
|
||||
}
|
||||
server.authorizationToken(account, requestBody.toString()) { authorizationToken ->
|
||||
if (authorizationToken != null) {
|
||||
requestBuilder.addHeader("Authorization", authorizationToken)
|
||||
}
|
||||
|
||||
requestBuilder
|
||||
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(server.postUrl(contentType))
|
||||
.post(requestBody)
|
||||
val request = requestBuilder.build()
|
||||
requestBuilder
|
||||
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(server.postUrl(contentType))
|
||||
.post(requestBody)
|
||||
val request = requestBuilder.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
try {
|
||||
check(response.isSuccessful)
|
||||
response.body.use { body ->
|
||||
val url = server.parseUrlFromSuccess(body.string())
|
||||
checkNotNull(url) {
|
||||
"There must be an uploaded image URL in the response"
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
try {
|
||||
check(response.isSuccessful)
|
||||
response.body.use { body ->
|
||||
val url = server.parseUrlFromSuccess(body.string(), authorizationToken)
|
||||
checkNotNull(url) {
|
||||
"There must be an uploaded image URL in the response"
|
||||
}
|
||||
|
||||
onSuccess(url, contentType)
|
||||
}
|
||||
|
||||
onSuccess(url, contentType)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError(e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
e.printStackTrace()
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
e.printStackTrace()
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun NIP98Header(url: String, method: String, body: String): String {
|
||||
val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: ""
|
||||
fun NIP98Header(url: String, method: String, body: String, onReady: (String?) -> Unit) {
|
||||
val myAccount = account
|
||||
|
||||
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
|
||||
return "Nostr " + encodedNIP98Event
|
||||
if (myAccount == null) {
|
||||
onReady(null)
|
||||
return
|
||||
}
|
||||
|
||||
myAccount.createHTTPAuthorization(url, method, body) {
|
||||
val noteJson = it.toJson()
|
||||
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
|
||||
onReady("Nostr $encodedNIP98Event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FileServer {
|
||||
abstract fun postUrl(contentType: String?): String
|
||||
abstract fun parseUrlFromSuccess(body: String): String?
|
||||
abstract fun parseUrlFromSuccess(body: String, authorizationToken: String?): String?
|
||||
abstract fun inputParameterName(contentType: String?): String
|
||||
|
||||
open fun clientID(info: String): String? = null
|
||||
open fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
||||
onReady(null)
|
||||
}
|
||||
}
|
||||
|
||||
class NostrImgServer : FileServer() {
|
||||
override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload"
|
||||
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
val url = tree?.get("data")?.get("link")?.asText()
|
||||
return url
|
||||
@ -172,8 +183,6 @@ class NostrImgServer : FileServer() {
|
||||
override fun inputParameterName(contentType: String?): String {
|
||||
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||
}
|
||||
|
||||
override fun clientID(info: String) = null
|
||||
}
|
||||
|
||||
class ImgurServer : FileServer() {
|
||||
@ -182,7 +191,7 @@ class ImgurServer : FileServer() {
|
||||
return if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
||||
}
|
||||
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
val url = tree?.get("data")?.get("link")?.asText()
|
||||
return url
|
||||
@ -192,12 +201,14 @@ class ImgurServer : FileServer() {
|
||||
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||
}
|
||||
|
||||
override fun clientID(info: String) = "Client-ID e6aea87296f3f96"
|
||||
override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
||||
onReady("Client-ID e6aea87296f3f96")
|
||||
}
|
||||
}
|
||||
|
||||
class NostrBuildServer : FileServer() {
|
||||
override fun postUrl(contentType: String?) = "https://nostr.build/api/v2/upload/files"
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
val data = tree?.get("data")
|
||||
val data0 = data?.get(0)
|
||||
@ -209,12 +220,14 @@ class NostrBuildServer : FileServer() {
|
||||
return "file"
|
||||
}
|
||||
|
||||
override fun clientID(info: String) = ImageUploader.NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info)
|
||||
override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
||||
ImageUploader(account).NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
class NostrFilesDevServer : FileServer() {
|
||||
override fun postUrl(contentType: String?) = "https://nostrfiles.dev/upload_image"
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
return tree?.get("url")?.asText()
|
||||
}
|
||||
@ -222,26 +235,29 @@ class NostrFilesDevServer : FileServer() {
|
||||
override fun inputParameterName(contentType: String?): String {
|
||||
return "file"
|
||||
}
|
||||
|
||||
override fun clientID(info: String) = null
|
||||
}
|
||||
|
||||
class NostrCheckMeServer : FileServer() {
|
||||
override fun postUrl(contentType: String?) = "https://nostrcheck.me/api/v1/media"
|
||||
override fun parseUrlFromSuccess(body: String): String? {
|
||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
||||
checkNotInMainThread()
|
||||
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
val url = tree?.get("url")?.asText()
|
||||
var id = tree?.get("id")?.asText()
|
||||
val id = tree?.get("id")?.asText()
|
||||
var isCompleted = false
|
||||
|
||||
val client = HttpClient.getHttpClient()
|
||||
var requrl = "https://nostrcheck.me/api/v1/media?id=" + id // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a"
|
||||
val requrl =
|
||||
"https://nostrcheck.me/api/v1/media?id=$id" // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(requrl)
|
||||
.addHeader("Authorization", ImageUploader.NIP98Header(requrl, "GET", ""))
|
||||
.get()
|
||||
.build()
|
||||
val requestBuilder = Request.Builder().url(requrl)
|
||||
|
||||
if (authorizationToken != null) {
|
||||
requestBuilder.addHeader("Authorization", authorizationToken)
|
||||
}
|
||||
|
||||
val request = requestBuilder.get().build()
|
||||
|
||||
while (!isCompleted) {
|
||||
client.newCall(request).execute().use {
|
||||
@ -261,5 +277,7 @@ class NostrCheckMeServer : FileServer() {
|
||||
return "mediafile"
|
||||
}
|
||||
|
||||
override fun clientID(body: String) = ImageUploader.NIP98Header("https://nostrcheck.me/api/v1/media", "POST", body)
|
||||
override fun authorizationToken(account: Account?, body: String, onReady: (String?) -> Unit) {
|
||||
ImageUploader(account).NIP98Header("https://nostrcheck.me/api/v1/media", "POST", body, onReady)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Uploading"
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
@ -173,11 +173,12 @@ open class NewMediaModel : ViewModel() {
|
||||
onReady = {
|
||||
uploadingPercentage.value = 0.90f
|
||||
uploadingDescription.value = "Sending"
|
||||
account?.sendHeader(it, relayList)
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
onceUploaded()
|
||||
cancel()
|
||||
account?.sendHeader(it, relayList) {
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
onceUploaded()
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
cancel()
|
||||
@ -216,18 +217,16 @@ open class NewMediaModel : ViewModel() {
|
||||
onReady = {
|
||||
uploadingDescription.value = "Signing"
|
||||
uploadingPercentage.value = 0.40f
|
||||
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
||||
|
||||
if (nip95 != null) {
|
||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
||||
uploadingDescription.value = "Sending"
|
||||
uploadingPercentage.value = 0.60f
|
||||
account?.sendNip95(nip95.first, nip95.second, relayList)
|
||||
}
|
||||
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
onceUploaded()
|
||||
cancel()
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
onceUploaded()
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
uploadingDescription.value = null
|
||||
|
@ -199,8 +199,11 @@ fun NewPostView(
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
NostrSearchEventOrUserDataSource.start()
|
||||
|
||||
onDispose {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
NostrSearchEventOrUserDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,6 +124,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var nip24 by mutableStateOf(false)
|
||||
|
||||
open fun load(accountViewModel: AccountViewModel, replyingTo: Note?, quote: Note?) {
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
|
||||
originalNote = replyingTo
|
||||
replyingTo?.let { replyNote ->
|
||||
if (replyNote.event is BaseTextNoteEvent) {
|
||||
@ -167,9 +170,6 @@ open class NewPostViewModel() : ViewModel() {
|
||||
zapRaiserAmount = null
|
||||
forwardZapTo = Split()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
this.accountViewModel = accountViewModel
|
||||
this.account = accountViewModel.account
|
||||
}
|
||||
|
||||
fun sendPost(relayList: List<Relay>? = null) {
|
||||
@ -326,7 +326,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
@ -556,17 +556,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val note = account?.sendHeader(it, relayList = relayList)
|
||||
account?.sendHeader(it, relayList = relayList) { note ->
|
||||
isUploadingImage = false
|
||||
|
||||
isUploadingImage = false
|
||||
|
||||
if (note == null) {
|
||||
message = TextFieldValue(message.text + "\n" + imageUrl)
|
||||
} else {
|
||||
message = TextFieldValue(message.text + "\nnostr:" + note.toNEvent())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
@ -587,16 +583,17 @@ open class NewPostViewModel() : ViewModel() {
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
val nip95 = account?.createNip95(bytes, headerInfo = it)
|
||||
val note = nip95?.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) }
|
||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
||||
val note = nip95.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) }
|
||||
|
||||
isUploadingImage = false
|
||||
isUploadingImage = false
|
||||
|
||||
note?.let {
|
||||
message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent())
|
||||
note?.let {
|
||||
message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
|
@ -184,7 +184,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader.uploadImage(
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
|
@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -155,19 +156,33 @@ fun InvoiceRequest(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
|
||||
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount * 1000,
|
||||
message,
|
||||
zapRequest?.toJson(),
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
},
|
||||
context = context
|
||||
)
|
||||
if (account.defaultZapType == LnZapEvent.ZapType.NONZAP) {
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount * 1000,
|
||||
message,
|
||||
null,
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
},
|
||||
context = context
|
||||
)
|
||||
} else {
|
||||
account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType) { zapRequest ->
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount * 1000,
|
||||
message,
|
||||
zapRequest.toJson(),
|
||||
onSuccess = onSuccess,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
},
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
|
@ -3,12 +3,8 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class BookmarkPrivateFeedFilter(val account: Account) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().latestBookmarkList?.id ?: ""
|
||||
}
|
||||
@ -16,42 +12,18 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
|
||||
override fun feed(): List<Note> {
|
||||
val bookmarks = account.userProfile().latestBookmarkList
|
||||
|
||||
if (account.loginWithExternalSigner) {
|
||||
val id = bookmarks?.id
|
||||
if (id != null) {
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
|
||||
if (decryptedContent == null) {
|
||||
ExternalSignerUtils.decryptBookmark(
|
||||
bookmarks.content,
|
||||
account.keyPair.pubKey.toHexKey(),
|
||||
id
|
||||
)
|
||||
} else {
|
||||
bookmarks.decryptedContent = decryptedContent
|
||||
}
|
||||
}
|
||||
val decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id] ?: ""
|
||||
if (!account.isWriteable()) return emptyList()
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(decryptedContent)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
val addresses = bookmarks?.privateTaggedAddresses(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
val privateTags = bookmarks?.cachedPrivateTags() ?: return emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
} else {
|
||||
val privKey = account.keyPair.privKey ?: return emptyList()
|
||||
val notes = bookmarks.filterEvents(privateTags)
|
||||
.mapNotNull { LocalCache.checkGetOrCreateNote(it) }
|
||||
|
||||
val notes = bookmarks?.privateTaggedEvents(privKey)
|
||||
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
|
||||
val addresses = bookmarks.filterAddresses(privateTags)
|
||||
.map { LocalCache.getOrCreateAddressableNote(it) }
|
||||
|
||||
val addresses = bookmarks?.privateTaggedAddresses(privKey)
|
||||
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
|
||||
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
return notes.plus(addresses).toSet()
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,9 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
|
||||
object BookmarkPublicFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
class BookmarkPublicFeedFilter(val account: Account) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return BookmarkPrivateFeedFilter.account.userProfile().latestBookmarkList?.id ?: ""
|
||||
return account.userProfile().latestBookmarkList?.id ?: ""
|
||||
}
|
||||
override fun feed(): List<Note> {
|
||||
val bookmarks = account.userProfile().latestBookmarkList
|
||||
|
@ -12,11 +12,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -33,12 +33,12 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet()
|
||||
|
||||
val createEvents = collection.filter { it.event is ChannelCreateEvent }
|
||||
val anyOtherChannelEvent = collection
|
||||
@ -60,7 +60,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
||||
|
||||
val counter = ParticipantListBuilder()
|
||||
val participantCounts = collection.associate {
|
||||
|
@ -12,11 +12,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -33,12 +33,12 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet()
|
||||
|
||||
val createEvents = collection.filter { it.event is CommunityDefinitionEvent }
|
||||
val anyOtherCommunityEvent = collection
|
||||
@ -60,16 +60,21 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
||||
|
||||
val counter = ParticipantListBuilder()
|
||||
val participantCounts = collection.associate {
|
||||
it to counter.countFollowsThatParticipateOn(it, followingKeySet)
|
||||
}
|
||||
|
||||
val allParticipants = collection.associate {
|
||||
it to counter.countFollowsThatParticipateOn(it, null)
|
||||
}
|
||||
|
||||
return collection.sortedWith(
|
||||
compareBy(
|
||||
{ participantCounts[it] },
|
||||
{ allParticipants[it] },
|
||||
{ it.createdAt() },
|
||||
{ it.idHex }
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ open class DiscoverLiveFeedFilter(
|
||||
}
|
||||
|
||||
open fun followList(): String {
|
||||
return account.defaultDiscoveryFollowList
|
||||
return account.defaultDiscoveryFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
@ -43,12 +43,12 @@ open class DiscoverLiveFeedFilter(
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(followList()) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(followList()) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(followList()) ?: emptySet()
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet()
|
||||
|
||||
val activities = collection
|
||||
.asSequence()
|
||||
@ -68,17 +68,22 @@ open class DiscoverLiveFeedFilter(
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val followingKeySet = account.selectedUsersFollowList(followList())
|
||||
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: account.liveKind3Follows.value.users
|
||||
|
||||
val counter = ParticipantListBuilder()
|
||||
val participantCounts = collection.associate {
|
||||
it to counter.countFollowsThatParticipateOn(it, followingKeySet)
|
||||
}
|
||||
|
||||
val allParticipants = collection.associate {
|
||||
it to counter.countFollowsThatParticipateOn(it, null)
|
||||
}
|
||||
|
||||
return collection.sortedWith(
|
||||
compareBy(
|
||||
{ convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) },
|
||||
{ participantCounts[it] },
|
||||
{ allParticipants[it] },
|
||||
{ (it.event as? LiveActivitiesEvent)?.starts() ?: it.createdAt() },
|
||||
{ it.idHex }
|
||||
)
|
||||
|
@ -14,21 +14,9 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<User> {
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(decryptedContent)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateUsers(account.keyPair.privKey)
|
||||
?.map { LocalCache.getOrCreateUser(it) }
|
||||
?: emptyList()
|
||||
return account.liveHiddenUsers.value?.hiddenUsers?.map {
|
||||
LocalCache.getOrCreateUser(it)
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,19 +30,7 @@ class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
|
||||
}
|
||||
|
||||
override fun feed(): List<String> {
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(decryptedContent)?.toList()
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(account.keyPair.privKey)?.toList()
|
||||
?: emptyList()
|
||||
return account.liveHiddenUsers.value?.hiddenWords?.toList() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -30,12 +30,12 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingGeoHashSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet()
|
||||
|
||||
val now = TimeUtils.now()
|
||||
|
||||
@ -43,7 +43,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
.asSequence()
|
||||
.filter {
|
||||
(it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false || it.event?.isTaggedGeoHashes(followingGeoHashSet) ?: false) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false || it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
(isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) &&
|
||||
((it.event?.createdAt() ?: 0) < now) &&
|
||||
|
@ -19,11 +19,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultHomeFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -38,14 +38,14 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>, ignoreAddressables: Boolean): Set<Note> {
|
||||
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultHomeFollowList.value == GLOBAL_FOLLOWS
|
||||
val gRelays = account.activeGlobalRelays()
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingGeoSet = account.selectedGeohashesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveHomeFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveHomeFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveHomeFollowLists.value?.geotags ?: emptySet()
|
||||
val followingCommunities = account.liveHomeFollowLists.value?.communities ?: emptySet()
|
||||
|
||||
val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
|
||||
val oneHr = 60 * 60
|
||||
@ -57,7 +57,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
val isGlobalRelay = it.relays.any { gRelays.contains(it) }
|
||||
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent) &&
|
||||
(!ignoreAddressables || noteEvent.kind() < 10000) &&
|
||||
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
||||
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeohashSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
(isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) &&
|
||||
((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
|
||||
|
@ -20,11 +20,11 @@ import com.vitorpamplona.quartz.events.RepostEvent
|
||||
|
||||
class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultNotificationFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultNotificationFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -36,10 +36,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS
|
||||
val isGlobal = account.defaultNotificationFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveNotificationFollowLists.value?.users ?: emptySet()
|
||||
|
||||
val loggedInUser = account.userProfile()
|
||||
val loggedInUserHex = loggedInUser.pubkeyHex
|
||||
|
@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ThreadAssembler
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@ -16,14 +15,14 @@ class ThreadFeedFilter(val account: Account, val noteId: String) : FeedFilter<No
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val cachedSignatures: MutableMap<Note, Note.LevelSignature> = mutableMapOf()
|
||||
val followingSet = account.selectedUsersFollowList(KIND3_FOLLOWS) ?: emptySet()
|
||||
val followingKeySet = account.liveKind3Follows.value.users
|
||||
val eventsToWatch = ThreadAssembler().findThreadFor(noteId)
|
||||
val eventsInHex = eventsToWatch.map { it.idHex }.toSet()
|
||||
val now = TimeUtils.now()
|
||||
|
||||
// Currently orders by date of each event, descending, at each level of the reply stack
|
||||
val order = compareByDescending<Note> {
|
||||
it.replyLevelSignature(eventsInHex, cachedSignatures, account.userProfile(), followingSet, now).signature
|
||||
it.replyLevelSignature(eventsInHex, cachedSignatures, account.userProfile(), followingKeySet, now).signature
|
||||
}
|
||||
|
||||
return eventsToWatch.sortedWith(order)
|
||||
|
@ -11,11 +11,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList.value
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
return account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@ -30,12 +30,12 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
val isGlobal = account.defaultStoriesFollowList.value == GLOBAL_FOLLOWS
|
||||
val isHiddenList = account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
val followingKeySet = account.liveStoriesFollowLists.value?.users ?: emptySet()
|
||||
val followingTagSet = account.liveStoriesFollowLists.value?.hashtags ?: emptySet()
|
||||
val followingGeohashSet = account.liveStoriesFollowLists.value?.geotags ?: emptySet()
|
||||
|
||||
return collection
|
||||
.asSequence()
|
||||
|
@ -373,7 +373,7 @@ fun NoTopBar() {
|
||||
@Composable
|
||||
fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
val list by accountViewModel.account.defaultStoriesFollowList.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
@ -387,7 +387,7 @@ fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, ac
|
||||
@Composable
|
||||
fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
|
||||
val list by accountViewModel.account.defaultHomeFollowList.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
@ -405,7 +405,7 @@ fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accou
|
||||
@Composable
|
||||
fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
val list by accountViewModel.account.defaultNotificationFollowList.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
@ -419,7 +419,7 @@ fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerStat
|
||||
@Composable
|
||||
fun DiscoveryTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
val list by accountViewModel.account.defaultDiscoveryFollowList.collectAsStateWithLifecycle()
|
||||
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
@ -693,7 +693,7 @@ fun SimpleTextSpinner(
|
||||
id = R.string.select_an_option
|
||||
)
|
||||
|
||||
var currentText by remember(placeholderCode) {
|
||||
var currentText by remember(placeholderCode, options) {
|
||||
mutableStateOf(
|
||||
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption
|
||||
)
|
||||
|
@ -36,7 +36,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@ -62,12 +61,8 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||
@ -564,9 +559,7 @@ fun ListContent(
|
||||
conectOrbotDialogOpen = false
|
||||
disconnectTorDialog = false
|
||||
checked = true
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
enableTor(accountViewModel.account, true, proxyPort)
|
||||
}
|
||||
accountViewModel.enableTor(true, proxyPort)
|
||||
},
|
||||
onError = {
|
||||
accountViewModel.toast(
|
||||
@ -594,13 +587,7 @@ fun ListContent(
|
||||
onClick = {
|
||||
disconnectTorDialog = false
|
||||
checked = false
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
enableTor(
|
||||
accountViewModel.account,
|
||||
false,
|
||||
proxyPort
|
||||
)
|
||||
}
|
||||
accountViewModel.enableTor(false, proxyPort)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.yes))
|
||||
@ -619,17 +606,6 @@ fun ListContent(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enableTor(
|
||||
account: Account,
|
||||
checked: Boolean,
|
||||
portNumber: MutableState<String>
|
||||
) {
|
||||
account.proxyPort = portNumber.value.toInt()
|
||||
account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
ServiceManager.forceRestart()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayStatus(accountViewModel: AccountViewModel) {
|
||||
val connectedRelaysText by RelayPool.statusFlow.collectAsStateWithLifecycle(RelayPoolStatus(0, 0))
|
||||
|
@ -45,7 +45,6 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
@ -626,11 +625,11 @@ fun LoadModerators(
|
||||
}
|
||||
}
|
||||
|
||||
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
|
||||
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts)
|
||||
|
||||
val newParticipantUsers = if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts)
|
||||
|
||||
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||
@ -676,11 +675,12 @@ private fun LoadParticipants(
|
||||
} ?: emptyList<User>()
|
||||
)
|
||||
|
||||
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
|
||||
|
||||
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hostsAuthor)
|
||||
|
||||
val newParticipantUsers = if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hostsAuthor)
|
||||
|
||||
(hosts + followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||
@ -726,11 +726,11 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
|
||||
LaunchedEffect(key1 = channelUpdates) {
|
||||
launch(Dispatchers.IO) {
|
||||
val followingKeySet = accountViewModel.account.selectedUsersFollowList(accountViewModel.account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = accountViewModel.account.liveDiscoveryFollowLists.value?.users
|
||||
val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).toImmutableList()
|
||||
|
||||
val newParticipantUsers = if (followingKeySet == null) {
|
||||
val allFollows = accountViewModel.account.selectedUsersFollowList(KIND3_FOLLOWS)
|
||||
val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet()
|
||||
val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList()
|
||||
|
||||
(followingParticipants + (allParticipants - followingParticipants)).toImmutableList()
|
||||
|
@ -262,11 +262,6 @@ private fun UserRoomCompose(
|
||||
note.createdAt()
|
||||
}
|
||||
}
|
||||
val content by remember(note) {
|
||||
mutableStateOf(
|
||||
accountViewModel.decrypt(note)
|
||||
)
|
||||
}
|
||||
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages.value != newHasNewMessages) {
|
||||
@ -274,22 +269,24 @@ private fun UserRoomCompose(
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelPicture = {
|
||||
NonClickableUserPictures(
|
||||
users = room.users,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size55dp
|
||||
)
|
||||
},
|
||||
channelTitle = {
|
||||
RoomNameDisplay(room, it, accountViewModel)
|
||||
},
|
||||
channelLastTime = createAt,
|
||||
channelLastContent = content,
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
LoadDecryptedContentOrNull(note, accountViewModel) { content ->
|
||||
ChannelName(
|
||||
channelPicture = {
|
||||
NonClickableUserPictures(
|
||||
users = room.users,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size55dp
|
||||
)
|
||||
},
|
||||
channelTitle = {
|
||||
RoomNameDisplay(room, it, accountViewModel)
|
||||
},
|
||||
channelLastTime = createAt,
|
||||
channelLastContent = content,
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -630,17 +630,28 @@ private fun RenderRegularTextNote(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val tags = remember(note.event) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val modifier = remember { Modifier.padding(top = 5.dp) }
|
||||
|
||||
if (eventContent != null) {
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
LoadDecryptedContentOrNull(note = note, accountViewModel = accountViewModel) { eventContent ->
|
||||
if (eventContent != null) {
|
||||
SensitivityWarning(
|
||||
note = note,
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent!!,
|
||||
canPreview = canPreview,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent!!,
|
||||
canPreview = canPreview,
|
||||
content = stringResource(id = R.string.could_not_decrypt_the_message),
|
||||
canPreview = true,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
@ -648,16 +659,6 @@ private fun RenderRegularTextNote(
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TranslatableRichTextViewer(
|
||||
content = stringResource(id = R.string.could_not_decrypt_the_message),
|
||||
canPreview = true,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
backgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1318,6 +1318,52 @@ fun authorRouteFor(note: Note): String {
|
||||
return "User/${note.author?.pubkeyHex}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadDecryptedContent(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String) -> Unit
|
||||
) {
|
||||
var decryptedContent by remember(note.event) {
|
||||
mutableStateOf(
|
||||
accountViewModel.cachedDecrypt(note)
|
||||
)
|
||||
}
|
||||
|
||||
decryptedContent?.let {
|
||||
inner(it)
|
||||
} ?: run {
|
||||
LaunchedEffect(key1 = decryptedContent) {
|
||||
accountViewModel.decrypt(note) {
|
||||
decryptedContent = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadDecryptedContentOrNull(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String?) -> Unit
|
||||
) {
|
||||
var decryptedContent by remember(note.event) {
|
||||
mutableStateOf(
|
||||
accountViewModel.cachedDecrypt(note)
|
||||
)
|
||||
}
|
||||
|
||||
if (decryptedContent == null) {
|
||||
LaunchedEffect(key1 = decryptedContent) {
|
||||
accountViewModel.decrypt(note) {
|
||||
decryptedContent = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner(decryptedContent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderTextEvent(
|
||||
note: Note,
|
||||
@ -1327,18 +1373,19 @@ fun RenderTextEvent(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val eventContent = remember(note.event) {
|
||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||
val body = accountViewModel.decrypt(note)
|
||||
LoadDecryptedContent(note, accountViewModel) { body ->
|
||||
val eventContent by remember(note.event) {
|
||||
derivedStateOf {
|
||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||
|
||||
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||
"### $subject\n$body"
|
||||
} else {
|
||||
body
|
||||
if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) {
|
||||
"### $subject\n$body"
|
||||
} else {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eventContent != null) {
|
||||
val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
@ -1636,17 +1683,16 @@ private fun RenderPrivateMessage(
|
||||
|
||||
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
|
||||
if (withMe) {
|
||||
val eventContent by remember { mutableStateOf(accountViewModel.decrypt(note)) }
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
LoadDecryptedContent(note, accountViewModel) { eventContent ->
|
||||
val hashtags = remember(note.event?.id()) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
|
||||
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
|
||||
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
if (eventContent != null) {
|
||||
if (makeItShort && isAuthorTheLoggedUser) {
|
||||
Text(
|
||||
text = eventContent!!,
|
||||
text = eventContent,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@ -1657,7 +1703,7 @@ private fun RenderPrivateMessage(
|
||||
accountViewModel = accountViewModel
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = eventContent!!,
|
||||
content = eventContent,
|
||||
canPreview = canPreview && !makeItShort,
|
||||
modifier = modifier,
|
||||
tags = tags,
|
||||
@ -1667,7 +1713,7 @@ private fun RenderPrivateMessage(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayUncitedHashtags(hashtags, eventContent!!, nav)
|
||||
DisplayUncitedHashtags(hashtags, eventContent, nav)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -2225,19 +2271,14 @@ private fun EmojiListOptions(
|
||||
}.distinctUntilChanged()
|
||||
}.observeAsState()
|
||||
|
||||
Crossfade(targetState = hasAddedThis) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Crossfade(targetState = hasAddedThis, label = "EmojiListOptions") {
|
||||
if (it != true) {
|
||||
AddButton() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote)
|
||||
}
|
||||
accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote)
|
||||
}
|
||||
} else {
|
||||
RemoveButton {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote)
|
||||
}
|
||||
accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2909,7 +2950,13 @@ private fun GenericRepostSection(
|
||||
baseAuthorPicture()
|
||||
}
|
||||
|
||||
Box(remember { Size18Modifier.align(Alignment.BottomStart).padding(1.dp) }) {
|
||||
Box(
|
||||
remember {
|
||||
Size18Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(1.dp)
|
||||
}
|
||||
) {
|
||||
RepostedIcon(modifier = Size18Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
|
||||
@ -3593,18 +3640,7 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
|
||||
|
||||
val defaultBackground = MaterialTheme.colorScheme.background
|
||||
val background = remember { mutableStateOf(defaultBackground) }
|
||||
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
val eventContent = remember(note.event) {
|
||||
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||
val body = accountViewModel.decrypt(note)
|
||||
|
||||
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||
"### $subject\n$body"
|
||||
} else {
|
||||
body
|
||||
}
|
||||
}
|
||||
val tags = remember(noteEvent) { noteEvent.tags()?.toImmutableListOfLists() ?: EmptyTagList }
|
||||
|
||||
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -143,8 +144,21 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
|
||||
}
|
||||
|
||||
if (showSelectTextDialog.value) {
|
||||
accountViewModel.decrypt(note)?.let {
|
||||
SelectTextDialog(it) { showSelectTextDialog.value = false }
|
||||
val decryptedNote = remember {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountViewModel.decrypt(note) {
|
||||
decryptedNote.value = it
|
||||
}
|
||||
}
|
||||
|
||||
decryptedNote.value?.let {
|
||||
SelectTextDialog(it) {
|
||||
showSelectTextDialog.value = false
|
||||
decryptedNote.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,15 +230,12 @@ private fun RenderMainPopup(
|
||||
icon = Icons.Default.ContentCopy,
|
||||
label = stringResource(R.string.quick_action_copy_text)
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(
|
||||
accountViewModel.decrypt(note) ?: ""
|
||||
)
|
||||
)
|
||||
accountViewModel.decrypt(note) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
showToast(R.string.copied_note_text_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(
|
||||
@ -278,10 +289,8 @@ private fun RenderMainPopup(
|
||||
stringResource(R.string.quick_action_delete)
|
||||
) {
|
||||
if (accountViewModel.hideDeleteRequestDialog) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
} else {
|
||||
showDeleteAlertDialog.value = true
|
||||
}
|
||||
@ -380,24 +389,18 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
QuickActionAlertDialog(
|
||||
title = stringResource(R.string.quick_action_request_deletion_alert_title),
|
||||
textContent = stringResource(R.string.quick_action_request_deletion_alert_body),
|
||||
buttonIcon = Icons.Default.Delete,
|
||||
buttonText = stringResource(R.string.quick_action_delete_dialog_btn),
|
||||
onClickDoOnce = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.delete(note)
|
||||
}
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
},
|
||||
onClickDontShowAgain = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.delete(note)
|
||||
accountViewModel.dontShowDeleteRequestDialog()
|
||||
}
|
||||
accountViewModel.delete(note)
|
||||
accountViewModel.dontShowDeleteRequestDialog()
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
|
@ -335,7 +335,7 @@ fun ZapVote(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp),
|
||||
onClick = {
|
||||
if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_send_zaps
|
||||
|
@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.events.CLOSED_AT
|
||||
import com.vitorpamplona.quartz.events.CONSENSUS_THRESHOLD
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.VALUE_MAXIMUM
|
||||
import com.vitorpamplona.quartz.events.VALUE_MINIMUM
|
||||
@ -68,10 +69,13 @@ class PollNoteViewModel : ViewModel() {
|
||||
fun refreshTallies() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
totalZapped = totalZapped()
|
||||
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
|
||||
wasZappedByLoggedInAccount = false
|
||||
account?.calculateIfNoteWasZappedByAccount(pollNote) {
|
||||
wasZappedByLoggedInAccount = true
|
||||
}
|
||||
|
||||
val newOptions = pollOptions?.keys?.map {
|
||||
val zappedInOption = zappedPollOptionAmount(it)
|
||||
val newOptions = pollOptions?.keys?.map { option ->
|
||||
val zappedInOption = zappedPollOptionAmount(option)
|
||||
|
||||
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
|
||||
zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP)
|
||||
@ -79,11 +83,11 @@ class PollNoteViewModel : ViewModel() {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
|
||||
val zappedByLoggedIn = account?.userProfile()?.let { it1 -> isPollOptionZappedBy(it, it1) } ?: false
|
||||
val cachedZappedByLoggedIn = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(option, it1) } ?: false
|
||||
|
||||
val consensus = consensusThreshold != null && myTally >= consensusThreshold!!
|
||||
|
||||
PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
|
||||
PollOption(option, pollOptions?.get(option) ?: "", zappedInOption, myTally, consensus, cachedZappedByLoggedIn)
|
||||
}
|
||||
|
||||
_tallies.emit(
|
||||
@ -166,11 +170,15 @@ class PollNoteViewModel : ViewModel() {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isPollOptionZappedBy(option: Int, user: User): Boolean {
|
||||
fun isPollOptionZappedBy(option: Int, user: User, onWasZappedByAuthor: () -> Unit) {
|
||||
pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor)
|
||||
}
|
||||
|
||||
fun cachedIsPollOptionZappedBy(option: Int, user: User): Boolean {
|
||||
return pollNote!!.zaps
|
||||
.any {
|
||||
val zapEvent = it.value?.event as? LnZapEvent
|
||||
val privateZapAuthor = account?.decryptZapContentAuthor(it.key)
|
||||
val privateZapAuthor = (it.key.event as? LnZapRequestEvent)?.cachedPrivateZap()
|
||||
zapEvent?.zappedPollOption() == option && (it.key.author?.pubkeyHex == user.pubkeyHex || privateZapAuthor?.pubKey == user.pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,6 @@ import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
@ -542,14 +541,10 @@ fun ReplyReaction(
|
||||
if (accountViewModel.isWriteable()) {
|
||||
onPress()
|
||||
} else {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onPress()
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_reply
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_reply
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
@ -849,14 +844,10 @@ private fun likeClick(
|
||||
R.string.no_reaction_type_setup_long_press_to_change
|
||||
)
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onWantsToSignReaction()
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_like_posts
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_like_posts
|
||||
)
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
@ -1067,7 +1058,7 @@ private fun zapClick(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
context.getString(R.string.no_zap_amount_setup_long_press_to_change)
|
||||
)
|
||||
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps)
|
||||
@ -1128,9 +1119,7 @@ private fun ObserveZapAmountText(
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
accountViewModel.calculateZapAmount(baseNote) { newZapAmount ->
|
||||
if (zapAmountTxt.value != newZapAmount) {
|
||||
withContext(Dispatchers.Main) {
|
||||
zapAmountTxt.value = newZapAmount
|
||||
}
|
||||
zapAmountTxt.value = newZapAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,13 +46,11 @@ import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -438,7 +436,9 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: ""))
|
||||
accountViewModel.decrypt(note) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
}
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@ -501,21 +501,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.remove_from_private_bookmarks))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.removePrivateBookmark(note, bookmarks?.decryptedContent ?: "")
|
||||
} else {
|
||||
accountViewModel.removePrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
accountViewModel.removePrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@ -524,21 +511,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.add_to_private_bookmarks))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.addPrivateBookmark(note, bookmarks?.decryptedContent ?: "")
|
||||
} else {
|
||||
accountViewModel.addPrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
accountViewModel.addPrivateBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -548,24 +522,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.remove_from_public_bookmarks))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.removePublicBookmark(
|
||||
note,
|
||||
bookmarks?.decryptedContent ?: ""
|
||||
)
|
||||
} else {
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@ -574,24 +532,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
Text(stringResource(R.string.add_to_public_bookmarks))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
val bookmarks = accountViewModel.userProfile().latestBookmarkList
|
||||
ExternalSignerUtils.decrypt(
|
||||
bookmarks?.content ?: "",
|
||||
accountViewModel.account.keyPair.pubKey.toHexKey(),
|
||||
bookmarks?.id ?: ""
|
||||
)
|
||||
bookmarks?.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[bookmarks?.id ?: ""] ?: ""
|
||||
accountViewModel.addPublicBookmark(
|
||||
note,
|
||||
bookmarks?.decryptedContent ?: ""
|
||||
)
|
||||
} else {
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -654,19 +596,21 @@ fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewMod
|
||||
|
||||
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newState = DropDownParams(
|
||||
isFollowingAuthor = accountViewModel.isFollowing(note.author),
|
||||
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
onNew(
|
||||
newState
|
||||
accountViewModel.isInPrivateBookmarks(note) {
|
||||
val newState = DropDownParams(
|
||||
isFollowingAuthor = accountViewModel.isFollowing(note.author),
|
||||
isPrivateBookmarkNote = it,
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
onNew(
|
||||
newState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,14 +198,10 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
if (isFollowing) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
}
|
||||
@ -213,14 +209,10 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.follow(baseAuthor)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.follow(baseAuthor)
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -9,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -19,12 +24,18 @@ import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AccountScreen(
|
||||
@ -44,15 +55,19 @@ fun AccountScreen(
|
||||
LoadingAccounts()
|
||||
}
|
||||
is AccountState.LoggedOff -> {
|
||||
LaunchedEffect(key1 = accountState) {
|
||||
serviceManager.pauseForGood()
|
||||
LaunchedEffect(key1 = state) {
|
||||
launch(Dispatchers.IO) {
|
||||
serviceManager.pauseForGood()
|
||||
}
|
||||
}
|
||||
|
||||
LoginPage(accountStateViewModel, isFirstLogin = true)
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
LaunchedEffect(key1 = accountState) {
|
||||
serviceManager.restartIfDifferentAccount(state.account)
|
||||
LaunchedEffect(key1 = state) {
|
||||
launch(Dispatchers.IO) {
|
||||
serviceManager.restartIfDifferentAccount(state.account)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
@ -66,8 +81,10 @@ fun AccountScreen(
|
||||
}
|
||||
}
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
LaunchedEffect(key1 = accountState) {
|
||||
serviceManager.restartIfDifferentAccount(state.account)
|
||||
LaunchedEffect(key1 = state) {
|
||||
launch(Dispatchers.IO) {
|
||||
serviceManager.restartIfDifferentAccount(state.account)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
@ -98,6 +115,51 @@ fun LoggedInPage(
|
||||
)
|
||||
)
|
||||
|
||||
val activity = getActivity() as MainActivity
|
||||
// Find a better way to associate these two.
|
||||
accountViewModel.serviceManager = activity.serviceManager
|
||||
|
||||
if (accountViewModel.account.signer is NostrSignerExternal) {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
accountViewModel.toast(
|
||||
R.string.sign_request_rejected,
|
||||
R.string.sign_request_rejected_description
|
||||
)
|
||||
} else {
|
||||
result.data?.let {
|
||||
accountViewModel.runOnIO {
|
||||
accountViewModel.account.signer.launcher.newResult(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity) {
|
||||
accountViewModel.account.signer.launcher.registerLauncher(
|
||||
launcher = {
|
||||
try {
|
||||
activity.prepareToLaunchSigner()
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
accountViewModel.toast(
|
||||
R.string.error_opening_external_signer,
|
||||
R.string.error_opening_external_signer_description
|
||||
)
|
||||
}
|
||||
},
|
||||
contentResolver = { Amethyst.instance.contentResolver }
|
||||
)
|
||||
onDispose {
|
||||
accountViewModel.account.signer.launcher.clearLauncher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel)
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,9 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@ -49,7 +52,12 @@ class AccountStateViewModel() : ViewModel() {
|
||||
_accountContent.update { AccountState.LoggedOff }
|
||||
}
|
||||
|
||||
suspend fun loginAndStartUI(key: String, useProxy: Boolean, proxyPort: Int, loginWithExternalSigner: Boolean = false) = withContext(Dispatchers.IO) {
|
||||
suspend fun loginAndStartUI(
|
||||
key: String,
|
||||
useProxy: Boolean,
|
||||
proxyPort: Int,
|
||||
loginWithExternalSigner: Boolean = false
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val parsed = Nip19.uriToRoute(key)
|
||||
val pubKeyParsed = parsed?.hex?.hexToByteArray()
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
@ -60,16 +68,21 @@ class AccountStateViewModel() : ViewModel() {
|
||||
|
||||
val account =
|
||||
if (loginWithExternalSigner) {
|
||||
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort, loginWithExternalSigner = true)
|
||||
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerExternal(keyPair.pubKey.toHexKey()))
|
||||
} else if (key.startsWith("nsec")) {
|
||||
Account(KeyPair(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort)
|
||||
val keyPair = KeyPair(privKey = key.bechToBytes())
|
||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||
} else if (pubKeyParsed != null) {
|
||||
Account(KeyPair(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort)
|
||||
val keyPair = KeyPair(pubKey = pubKeyParsed)
|
||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
||||
val keyPair = KeyPair()
|
||||
// Evaluate NIP-5
|
||||
Account(KeyPair(), proxy = proxy, proxyPort = proxyPort)
|
||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||
} else {
|
||||
Account(KeyPair(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort)
|
||||
val keyPair = KeyPair(Hex.decode(key))
|
||||
Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||
}
|
||||
|
||||
LocalPreferences.updatePrefsForLogin(account)
|
||||
@ -78,7 +91,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
suspend fun startUI(account: Account) = withContext(Dispatchers.Main) {
|
||||
if (account.keyPair.privKey != null) {
|
||||
if (account.isWriteable()) {
|
||||
_accountContent.update { AccountState.LoggedIn(account) }
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
@ -132,7 +145,8 @@ class AccountStateViewModel() : ViewModel() {
|
||||
fun newKey(useProxy: Boolean, proxyPort: Int) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
val account = Account(KeyPair(), proxy = proxy, proxyPort = proxyPort)
|
||||
val keyPair = KeyPair()
|
||||
val account = Account(keyPair, proxy = proxy, proxyPort = proxyPort, signer = NostrSignerInternal(keyPair))
|
||||
|
||||
account.follow(account.userProfile())
|
||||
|
||||
|
@ -191,8 +191,23 @@ class NostrHomeRepliesFeedViewModel(val account: Account) : FeedViewModel(HomeCo
|
||||
}
|
||||
}
|
||||
|
||||
class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter)
|
||||
class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter)
|
||||
@Stable
|
||||
class NostrBookmarkPublicFeedViewModel(val account: Account) : FeedViewModel(BookmarkPublicFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrBookmarkPublicFeedViewModel : ViewModel> create(modelClass: Class<NostrBookmarkPublicFeedViewModel>): NostrBookmarkPublicFeedViewModel {
|
||||
return NostrBookmarkPublicFeedViewModel(account) as NostrBookmarkPublicFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NostrBookmarkPrivateFeedViewModel(val account: Account) : FeedViewModel(BookmarkPrivateFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrBookmarkPrivateFeedViewModel : ViewModel> create(modelClass: Class<NostrBookmarkPrivateFeedViewModel>): NostrBookmarkPrivateFeedViewModel {
|
||||
return NostrBookmarkPrivateFeedViewModel(account) as NostrBookmarkPrivateFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrUserAppRecommendationsFeedViewModel(val user: User) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) {
|
||||
class Factory(val user: User) : ViewModelProvider.Factory {
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.lifecycle.LiveData
|
||||
@ -15,6 +16,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
@ -25,6 +27,7 @@ import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
@ -68,7 +71,6 @@ import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@ -92,21 +94,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
|
||||
val toasts = MutableSharedFlow<ToastMsg?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val discoveryListLiveData = account.live.map {
|
||||
it.account.defaultDiscoveryFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val homeListLiveData = account.live.map {
|
||||
it.account.defaultHomeFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val notificationListLiveData = account.live.map {
|
||||
it.account.defaultNotificationFollowList
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val storiesListLiveData = account.live.map {
|
||||
it.account.defaultStoriesFollowList
|
||||
}.distinctUntilChanged()
|
||||
var serviceManager: ServiceManager? = null
|
||||
|
||||
val showSensitiveContentChanges = account.live.map {
|
||||
it.account.showSensitiveContent
|
||||
@ -134,15 +122,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
return account.isWriteable()
|
||||
}
|
||||
|
||||
fun loggedInWithExternalSigner(): Boolean {
|
||||
return account.loginWithExternalSigner
|
||||
}
|
||||
|
||||
fun userProfile(): User {
|
||||
return account.userProfile()
|
||||
}
|
||||
|
||||
fun reactTo(note: Note, reaction: String) {
|
||||
suspend fun reactTo(note: Note, reaction: String) {
|
||||
account.reactTo(note, reaction)
|
||||
}
|
||||
|
||||
@ -177,7 +161,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
return account.hasReacted(baseNote, reaction)
|
||||
}
|
||||
|
||||
fun deleteReactionTo(note: Note, reaction: String) {
|
||||
suspend fun deleteReactionTo(note: Note, reaction: String) {
|
||||
account.delete(account.reactionTo(note, reaction))
|
||||
}
|
||||
|
||||
@ -193,18 +177,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
|
||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
onWasZapped(account.calculateIfNoteWasZappedByAccount(zappedNote))
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote) {
|
||||
onWasZapped(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun calculateZapAmount(zappedNote: Note): BigDecimal {
|
||||
return account.calculateZappedAmount(zappedNote)
|
||||
}
|
||||
|
||||
suspend fun calculateZapAmount(zappedNote: Note, onZapAmount: suspend (String) -> Unit) {
|
||||
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
|
||||
if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
|
||||
account.calculateZappedAmount(zappedNote) {
|
||||
onZapAmount(showAmount(it))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onZapAmount(showAmount(zappedNote.zapsAmount))
|
||||
@ -214,20 +198,21 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
fun calculateZapraiser(zappedNote: Note, onZapraiserStatus: (ZapraiserStatus) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
val newZapAmount = calculateZapAmount(zappedNote)
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
account.calculateZappedAmount(zappedNote) { newZapAmount ->
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
if (percentage > 1) {
|
||||
percentage = 1f
|
||||
}
|
||||
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft = if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
val newZapraiserProgress = percentage
|
||||
val newZapraiserLeft = if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal())
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
}
|
||||
onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft))
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,14 +221,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val list = ArrayList<ZapAmountCommentNotification>(zaps.size)
|
||||
zaps.forEach {
|
||||
innerDecryptAmountMessage(it.request, it.response)?.let {
|
||||
list.add(it)
|
||||
allOrNothingSigningOperations<CombinedZap, ZapAmountCommentNotification>(
|
||||
remainingTos = zaps,
|
||||
runRequestFor = { next, onReady ->
|
||||
innerDecryptAmountMessage(next.request, next.response, onReady)
|
||||
}
|
||||
) {
|
||||
onNewState(it.toImmutableList())
|
||||
}
|
||||
|
||||
onNewState(list.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,14 +237,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val list = ArrayList<ZapAmountCommentNotification>(baseNote.zaps.size)
|
||||
baseNote.zaps.forEach {
|
||||
innerDecryptAmountMessage(it.key, it.value)?.let {
|
||||
list.add(it)
|
||||
allOrNothingSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
|
||||
remainingTos = baseNote.zaps.toList(),
|
||||
runRequestFor = { next, onReady ->
|
||||
innerDecryptAmountMessage(next.first, next.second, onReady)
|
||||
}
|
||||
) {
|
||||
onNewState(it.toImmutableList())
|
||||
}
|
||||
|
||||
onNewState(list.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,37 +254,43 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
onNewState: (ZapAmountCommentNotification?) -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onNewState(innerDecryptAmountMessage(zapRequest, zapEvent))
|
||||
innerDecryptAmountMessage(zapRequest, zapEvent, onNewState)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun innerDecryptAmountMessage(
|
||||
private fun innerDecryptAmountMessage(
|
||||
zapRequest: Note,
|
||||
zapEvent: Note?
|
||||
): ZapAmountCommentNotification? {
|
||||
zapEvent: Note?,
|
||||
onReady: (ZapAmountCommentNotification) -> Unit
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
(zapRequest.event as? LnZapRequestEvent)?.let {
|
||||
val decryptedContent = decryptZap(zapRequest)
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (decryptedContent != null) {
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
return ZapAmountCommentNotification(
|
||||
newAuthor,
|
||||
decryptedContent.content.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
if (it.isPrivateZap()) {
|
||||
decryptZap(zapRequest) { decryptedContent ->
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||
onReady(
|
||||
ZapAmountCommentNotification(
|
||||
newAuthor,
|
||||
decryptedContent.content.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val amount = (zapEvent?.event as? LnZapEvent)?.amount
|
||||
if (!zapRequest.event?.content().isNullOrBlank() || amount != null) {
|
||||
return ZapAmountCommentNotification(
|
||||
zapRequest.author,
|
||||
zapRequest.event?.content()?.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
onReady(
|
||||
ZapAmountCommentNotification(
|
||||
zapRequest.author,
|
||||
zapRequest.event?.content()?.ifBlank { null },
|
||||
showAmountAxis(amount)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun zap(
|
||||
@ -319,7 +310,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
|
||||
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
|
||||
account.report(note, type, content)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.report(note, type, content)
|
||||
}
|
||||
}
|
||||
|
||||
fun report(user: User, type: ReportEvent.ReportType) {
|
||||
@ -336,47 +329,43 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
|
||||
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
account.removeEmojiPack(usersEmojiList, emojiList)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.removeEmojiPack(usersEmojiList, emojiList)
|
||||
}
|
||||
}
|
||||
|
||||
fun addEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
account.addEmojiPack(usersEmojiList, emojiList)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.addEmojiPack(usersEmojiList, emojiList)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPrivateBookmark(note: Note) {
|
||||
account.addPrivateBookmark(note)
|
||||
}
|
||||
|
||||
fun addPrivateBookmark(note: Note, decryptedContent: String) {
|
||||
account.addPrivateBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun addPublicBookmark(note: Note, decryptedContent: String) {
|
||||
account.addPublicBookmark(note, decryptedContent)
|
||||
}
|
||||
|
||||
fun removePublicBookmark(note: Note, decryptedContent: String) {
|
||||
account.removePublicBookmark(note, decryptedContent)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.addBookmark(note, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPublicBookmark(note: Note) {
|
||||
account.addPublicBookmark(note)
|
||||
}
|
||||
|
||||
fun removePrivateBookmark(note: Note, decryptedContent: String) {
|
||||
account.removePrivateBookmark(note, decryptedContent)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.addBookmark(note, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun removePrivateBookmark(note: Note) {
|
||||
account.removePrivateBookmark(note)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.removeBookmark(note, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun removePublicBookmark(note: Note) {
|
||||
account.removePublicBookmark(note)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.removeBookmark(note, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInPrivateBookmarks(note: Note): Boolean {
|
||||
return account.isInPrivateBookmarks(note)
|
||||
fun isInPrivateBookmarks(note: Note, onReady: (Boolean) -> Unit) {
|
||||
account.isInPrivateBookmarks(note, onReady)
|
||||
}
|
||||
|
||||
fun isInPublicBookmarks(note: Note): Boolean {
|
||||
@ -388,15 +377,23 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
|
||||
fun delete(note: Note) {
|
||||
account.delete(note)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.delete(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun decrypt(note: Note): String? {
|
||||
return account.decryptContent(note)
|
||||
fun cachedDecrypt(note: Note): String? {
|
||||
return account.cachedDecryptContent(note)
|
||||
}
|
||||
|
||||
fun decryptZap(note: Note): Event? {
|
||||
return account.decryptZapContentAuthor(note)
|
||||
fun decrypt(note: Note, onReady: (String) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.decryptContent(note, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptZap(note: Note, onReady: (Event) -> Unit) {
|
||||
account.decryptZapContentAuthor(note, onReady)
|
||||
}
|
||||
|
||||
fun translateTo(lang: Locale) {
|
||||
@ -476,7 +473,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
get() = account.hideDeleteRequestDialog
|
||||
|
||||
fun dontShowDeleteRequestDialog() {
|
||||
account.setHideDeleteRequestDialog()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.setHideDeleteRequestDialog()
|
||||
}
|
||||
}
|
||||
|
||||
val hideNIP24WarningDialog: Boolean
|
||||
@ -551,11 +550,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
fun unwrap(event: GiftWrapEvent): Event? {
|
||||
return account.unwrap(event)
|
||||
fun unwrap(event: GiftWrapEvent, onReady: (Event) -> Unit) {
|
||||
account.unwrap(event, onReady)
|
||||
}
|
||||
fun unseal(event: SealedGossipEvent): Event? {
|
||||
return account.unseal(event)
|
||||
fun unseal(event: SealedGossipEvent, onReady: (Event) -> Unit) {
|
||||
account.unseal(event, onReady)
|
||||
}
|
||||
|
||||
fun show(user: User) {
|
||||
@ -859,6 +858,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
fun enableTor(
|
||||
checked: Boolean,
|
||||
portNumber: MutableState<String>
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.proxyPort = portNumber.value.toInt()
|
||||
account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
account.saveable.invalidateData()
|
||||
serviceManager?.forceRestart()
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory {
|
||||
override fun <AccountViewModel : ViewModel> create(modelClass: Class<AccountViewModel>): AccountViewModel {
|
||||
return AccountViewModel(account, settings) as AccountViewModel
|
||||
@ -866,7 +877,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
val notificationDots = HasNotificationDot(bottomNavigationItems, account)
|
||||
val notificationDots = HasNotificationDot(bottomNavigationItems)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(3000, Dispatchers.IO)
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
@ -875,9 +886,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
|
||||
fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
notificationDots.update(newNotes)
|
||||
notificationDots.update(newNotes, account)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes")
|
||||
}
|
||||
@ -886,7 +897,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
Log.d("Init", "AccountViewModel")
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d("Rendering Metrics", "Notification Dots Calculation refresh ${this@AccountViewModel}")
|
||||
Log.d("Rendering Metrics", "Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}")
|
||||
invalidateInsertData(newNotes)
|
||||
}
|
||||
}
|
||||
@ -936,26 +947,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
||||
onMore()
|
||||
}
|
||||
} else {
|
||||
if (loggedInWithExternalSigner()) {
|
||||
if (hasBoosted(baseNote)) {
|
||||
deleteBoostsTo(baseNote)
|
||||
} else {
|
||||
onMore()
|
||||
}
|
||||
} else {
|
||||
toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_boost_posts
|
||||
)
|
||||
}
|
||||
toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_boost_posts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>, val account: Account) {
|
||||
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
|
||||
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
|
||||
|
||||
fun update(newNotes: Set<Note>) {
|
||||
fun update(newNotes: Set<Note>, account: Account) {
|
||||
checkNotInMainThread()
|
||||
|
||||
hasNewItems.forEach {
|
||||
@ -972,3 +975,22 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>, val accoun
|
||||
|
||||
@Immutable
|
||||
data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19.Return)
|
||||
|
||||
public fun <T, K> allOrNothingSigningOperations(
|
||||
remainingTos: List<T>,
|
||||
runRequestFor: (T, (K) -> Unit) -> Unit,
|
||||
output: MutableList<K> = mutableListOf(),
|
||||
onReady: (List<K>) -> Unit
|
||||
) {
|
||||
if (remainingTos.isEmpty()) {
|
||||
onReady(output)
|
||||
return
|
||||
}
|
||||
|
||||
val next = remainingTos.first()
|
||||
|
||||
runRequestFor(next) { result: K ->
|
||||
output.add(result)
|
||||
allOrNothingSigningOperations(remainingTos.minus(next), runRequestFor, output, onReady)
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
@ -29,13 +27,17 @@ import kotlinx.coroutines.launch
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
BookmarkPublicFeedFilter.account = accountViewModel.account
|
||||
BookmarkPrivateFeedFilter.account = accountViewModel.account
|
||||
val publicFeedViewModel: NostrBookmarkPublicFeedViewModel = viewModel(
|
||||
key = "NotificationViewModel",
|
||||
factory = NostrBookmarkPublicFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val publicFeedViewModel: NostrBookmarkPublicFeedViewModel = viewModel()
|
||||
val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel = viewModel()
|
||||
val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel = viewModel(
|
||||
key = "NotificationViewModel",
|
||||
factory = NostrBookmarkPrivateFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val userState by accountViewModel.account.userProfile().live().bookmarks.observeAsState()
|
||||
val userState by accountViewModel.account.decryptBookmarks.observeAsState()
|
||||
|
||||
LaunchedEffect(userState) {
|
||||
publicFeedViewModel.invalidateData()
|
||||
|
@ -25,7 +25,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@ -173,7 +172,8 @@ private fun RenderDiscoverFeed(
|
||||
|
||||
Crossfade(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
label = "RenderDiscoverFeed"
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
@ -213,9 +213,9 @@ fun WatchAccountForDiscoveryScreen(
|
||||
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultDiscoveryFollowList) {
|
||||
LaunchedEffect(accountViewModel, listState) {
|
||||
NostrDiscoveryDataSource.resetFilters()
|
||||
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
|
@ -169,14 +169,10 @@ fun GeoHashActionOptions(
|
||||
if (isFollowingTag) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.unfollowGeohash(tag)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.unfollowGeohash(tag)
|
||||
}
|
||||
@ -184,14 +180,10 @@ fun GeoHashActionOptions(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.followGeohash(tag)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.followGeohash(tag)
|
||||
}
|
||||
|
@ -142,14 +142,10 @@ fun HashtagActionOptions(
|
||||
if (isFollowingTag) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.unfollowHashtag(tag)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.unfollowHashtag(tag)
|
||||
}
|
||||
@ -157,14 +153,10 @@ fun HashtagActionOptions(
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.followHashtag(tag)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.followHashtag(tag)
|
||||
}
|
||||
|
@ -305,14 +305,10 @@ fun MutedWordActionOptions(
|
||||
if (isMutedWord == true) {
|
||||
ShowWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.showWord(word)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_show_word
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_show_word
|
||||
)
|
||||
} else {
|
||||
accountViewModel.showWord(word)
|
||||
}
|
||||
@ -320,14 +316,10 @@ fun MutedWordActionOptions(
|
||||
} else {
|
||||
HideWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.hideWord(word)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_hide_word
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_hide_word
|
||||
)
|
||||
} else {
|
||||
accountViewModel.hideWord(word)
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@ -25,6 +24,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
@ -216,10 +216,9 @@ fun WatchAccountForHomeScreen(
|
||||
repliesFeedViewModel: NostrHomeRepliesFeedViewModel,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val followState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultHomeFollowList, followState) {
|
||||
LaunchedEffect(accountViewModel, homeFollowList) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
homeFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
repliesFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
|
@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -75,36 +77,12 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav:
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
val note = noteState?.note ?: return@LaunchedEffect
|
||||
var event = note.event
|
||||
val channelHex = note.channelHex()
|
||||
val event = note.event
|
||||
|
||||
if (event is GiftWrapEvent) {
|
||||
event = accountViewModel.unwrap(event)
|
||||
}
|
||||
|
||||
if (event is SealedGossipEvent) {
|
||||
event = accountViewModel.unseal(event)
|
||||
}
|
||||
|
||||
if (event == null) {
|
||||
// stay here, loading
|
||||
} else if (event is ChannelCreateEvent) {
|
||||
nav("Channel/${note.idHex}")
|
||||
} else if (event is ChatroomKeyable) {
|
||||
note.author?.let {
|
||||
val withKey = (event as ChatroomKeyable)
|
||||
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.userProfile().createChatroom(withKey)
|
||||
}
|
||||
|
||||
nav("Room/${withKey.hashCode()}")
|
||||
if (event != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
redirect(event, note, accountViewModel, nav)
|
||||
}
|
||||
} else if (channelHex != null) {
|
||||
nav("Channel/$channelHex")
|
||||
} else {
|
||||
nav("Note/${note.idHex}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,3 +97,36 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav:
|
||||
Text(stringResource(R.string.looking_for_event, baseNote.idHex))
|
||||
}
|
||||
}
|
||||
|
||||
fun redirect(event: EventInterface, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val channelHex = note.channelHex()
|
||||
|
||||
if (event is GiftWrapEvent) {
|
||||
accountViewModel.unwrap(event) {
|
||||
redirect(it, note, accountViewModel, nav)
|
||||
}
|
||||
} else if (event is SealedGossipEvent) {
|
||||
accountViewModel.unseal(event) {
|
||||
redirect(it, note, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
if (event == null) {
|
||||
// stay here, loading
|
||||
} else if (event is ChannelCreateEvent) {
|
||||
nav("Channel/${note.idHex}")
|
||||
} else if (event is ChatroomKeyable) {
|
||||
note.author?.let {
|
||||
val withKey = (event as ChatroomKeyable)
|
||||
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
|
||||
accountViewModel.userProfile().createChatroom(withKey)
|
||||
|
||||
nav("Room/${withKey.hashCode()}")
|
||||
}
|
||||
} else if (channelHex != null) {
|
||||
nav("Channel/$channelHex")
|
||||
} else {
|
||||
nav("Note/${note.idHex}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -453,9 +453,7 @@ fun FloatingButtons(
|
||||
}
|
||||
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
|
||||
}
|
||||
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
|
||||
}
|
||||
is AccountState.LoggedOff -> {
|
||||
// Does nothing.
|
||||
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -141,9 +140,9 @@ fun WatchAccountForNotifications(
|
||||
notifFeedViewModel: NotificationViewModel,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultNotificationFollowList) {
|
||||
LaunchedEffect(accountViewModel, listState) {
|
||||
NostrAccountDataSource.invalidateFilters()
|
||||
notifFeedViewModel.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
|
@ -806,14 +806,10 @@ private fun DisplayFollowUnfollowButton(
|
||||
if (isLoggedInFollowingUser) {
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.unfollow(baseUser)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.unfollow(baseUser)
|
||||
}
|
||||
@ -822,14 +818,10 @@ private fun DisplayFollowUnfollowButton(
|
||||
if (isUserFollowingLoggedIn) {
|
||||
FollowButton(R.string.follow_back) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.follow(baseUser)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.follow(baseUser)
|
||||
}
|
||||
@ -837,14 +829,10 @@ private fun DisplayFollowUnfollowButton(
|
||||
} else {
|
||||
FollowButton(R.string.follow) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
accountViewModel.follow(baseUser)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
} else {
|
||||
accountViewModel.follow(baseUser)
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -45,8 +44,6 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.theme.WarningColor
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -82,8 +79,6 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
)
|
||||
}
|
||||
) { pad ->
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()),
|
||||
verticalArrangement = Arrangement.SpaceAround
|
||||
@ -99,10 +94,8 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
text = stringResource(R.string.report_dialog_block_hide_user_btn),
|
||||
icon = Icons.Default.Block,
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
SpacerH16()
|
||||
@ -138,15 +131,13 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss:
|
||||
icon = Icons.Default.Report,
|
||||
enabled = selectedReason in 0..reportTypes.lastIndex,
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.report(
|
||||
note,
|
||||
reportTypes[selectedReason].first,
|
||||
additionalReason
|
||||
)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.report(
|
||||
note,
|
||||
reportTypes[selectedReason].first,
|
||||
additionalReason
|
||||
)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -114,9 +114,9 @@ fun VideoScreen(
|
||||
|
||||
@Composable
|
||||
fun WatchAccountForVideoScreen(videoFeedView: NostrVideoFeedViewModel, accountViewModel: AccountViewModel) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, accountState?.account?.defaultStoriesFollowList) {
|
||||
LaunchedEffect(accountViewModel, listState) {
|
||||
NostrVideoDataSource.resetFilters()
|
||||
videoFeedView.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@ -32,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -68,16 +70,17 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.ExternalSignerUtils
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
import com.vitorpamplona.amethyst.service.SignerType
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.SignerType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -103,24 +106,62 @@ fun LoginPage(
|
||||
val scope = rememberCoroutineScope()
|
||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||
|
||||
val activity = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
loginWithExternalSigner = false
|
||||
ExternalSignerUtils.isActivityRunning = false
|
||||
ServiceManager.shouldPauseService = true
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
"Sign request rejected",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
if (loginWithExternalSigner) {
|
||||
val externalSignerLauncher = remember { ExternalSignerLauncher("") }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
"Sign request rejected",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
result.data?.let {
|
||||
externalSignerLauncher.newResult(it)
|
||||
}
|
||||
}
|
||||
return@rememberLauncherForActivityResult
|
||||
} else {
|
||||
val event = it.data?.getStringExtra("signature") ?: ""
|
||||
key.value = TextFieldValue(event)
|
||||
}
|
||||
)
|
||||
|
||||
val activity = getActivity() as MainActivity
|
||||
|
||||
DisposableEffect(launcher, activity, externalSignerLauncher) {
|
||||
externalSignerLauncher.registerLauncher(
|
||||
launcher = {
|
||||
try {
|
||||
activity.prepareToLaunchSigner()
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
Amethyst.instance,
|
||||
R.string.error_opening_external_signer,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
contentResolver = { Amethyst.instance.contentResolver }
|
||||
)
|
||||
onDispose {
|
||||
externalSignerLauncher.clearLauncher()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(loginWithExternalSigner, externalSignerLauncher) {
|
||||
externalSignerLauncher.openSignerApp(
|
||||
"",
|
||||
SignerType.GET_PUBLIC_KEY,
|
||||
"",
|
||||
""
|
||||
) { pubkey ->
|
||||
key.value = TextFieldValue(pubkey)
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
@ -137,18 +178,6 @@ fun LoginPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(loginWithExternalSigner) {
|
||||
if (loginWithExternalSigner) {
|
||||
ExternalSignerUtils.openSigner(
|
||||
"",
|
||||
SignerType.GET_PUBLIC_KEY,
|
||||
activity,
|
||||
"",
|
||||
""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
@ -400,7 +429,12 @@ fun LoginPage(
|
||||
return@Button
|
||||
}
|
||||
|
||||
val result = ExternalSignerUtils.getDataFromResolver(SignerType.GET_PUBLIC_KEY, arrayOf("login"))
|
||||
val result = ExternalSignerLauncher("").getDataFromResolver(
|
||||
SignerType.GET_PUBLIC_KEY,
|
||||
arrayOf("login"),
|
||||
contentResolver = Amethyst.instance.contentResolver
|
||||
)
|
||||
|
||||
if (result == null) {
|
||||
loginWithExternalSigner = true
|
||||
return@Button
|
||||
|
@ -601,7 +601,9 @@
|
||||
<string name="paid">Paid</string>
|
||||
<string name="wallet_number">Wallet %1$s</string>
|
||||
<string name="error_opening_external_signer">Error opening signer app</string>
|
||||
<string name="sign_request_rejected">Sign request rejected</string>
|
||||
<string name="error_opening_external_signer_description">The signer app could not be found. Check if the app hasn\'t been uninstalled</string>
|
||||
<string name="sign_request_rejected">Signer Application Rejected</string>
|
||||
<string name="sign_request_rejected_description">Make sure the signer application has authorized this transaction</string>
|
||||
|
||||
<string name="no_wallet_found_with_error">No Wallets found to pay a lightning invoice (Error: %1$s). Please install a Lightning wallet to use zaps</string>
|
||||
<string name="no_wallet_found">No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps</string>
|
||||
|
@ -13,6 +13,7 @@ import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.Gossip
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -43,7 +44,7 @@ class GiftWrapReceivingBenchmark {
|
||||
markAsSensitive = true,
|
||||
zapRaiserAmount = 10000,
|
||||
geohash = null,
|
||||
keyPair = sender
|
||||
signer = NostrSignerInternal(keyPair = sender)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class AdvertisedRelayListEvent(
|
||||
@ -40,9 +41,10 @@ class AdvertisedRelayListEvent(
|
||||
|
||||
fun create(
|
||||
list: List<AdvertisedRelayInfo>,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): AdvertisedRelayListEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (AdvertisedRelayListEvent) -> Unit
|
||||
) {
|
||||
val tags = list.map {
|
||||
if (it.type == AdvertisedRelayType.BOTH) {
|
||||
listOf(it.relayUrl)
|
||||
@ -51,10 +53,8 @@ class AdvertisedRelayListEvent(
|
||||
}
|
||||
}
|
||||
val msg = ""
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return AdvertisedRelayListEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
|
||||
|
||||
signer.sign(createdAt, kind, tags, msg, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
@Immutable
|
||||
@ -46,5 +48,14 @@ class AppDefinitionEvent(
|
||||
|
||||
companion object {
|
||||
const val kind = 31990
|
||||
|
||||
fun create(
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (AppDefinitionEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class AppRecommendationEvent(
|
||||
@ -19,5 +21,14 @@ class AppRecommendationEvent(
|
||||
|
||||
companion object {
|
||||
const val kind = 31989
|
||||
|
||||
fun create(
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (AppRecommendationEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class AudioHeaderEvent(
|
||||
@ -30,15 +29,16 @@ class AudioHeaderEvent(
|
||||
private const val STREAM_URL = "stream_url"
|
||||
private const val WAVEFORM = "waveform"
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
description: String,
|
||||
downloadUrl: String,
|
||||
streamUrl: String? = null,
|
||||
wavefront: String? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): AudioHeaderEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (AudioHeaderEvent) -> Unit
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
downloadUrl.let { listOf(DOWNLOAD_URL, it) },
|
||||
streamUrl?.let { listOf(STREAM_URL, it) },
|
||||
@ -52,11 +52,7 @@ class AudioHeaderEvent(
|
||||
}
|
||||
)
|
||||
|
||||
val content = description
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return AudioHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, description, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class AudioTrackEvent(
|
||||
@ -40,9 +41,10 @@ class AudioTrackEvent(
|
||||
price: String? = null,
|
||||
cover: String? = null,
|
||||
subject: String? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): AudioTrackEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (AudioTrackEvent) -> Unit
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
listOf(MEDIA, media),
|
||||
listOf(TYPE, type),
|
||||
@ -51,10 +53,7 @@ class AudioTrackEvent(
|
||||
subject?.let { listOf(SUBJECT, it) }
|
||||
)
|
||||
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return AudioTrackEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class BookmarkListEvent(
|
||||
@ -16,36 +17,156 @@ class BookmarkListEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient
|
||||
var decryptedContent = ""
|
||||
|
||||
companion object {
|
||||
const val kind = 30001
|
||||
|
||||
fun addEvent(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
eventId: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) = addTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addReplaceable(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
aTag: ATag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) = addTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun addTag(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
tagName: String,
|
||||
tagValue: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) {
|
||||
add(
|
||||
earlierVersion,
|
||||
listOf(listOf(tagName, tagValue)),
|
||||
isPrivate,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun add(
|
||||
earlierVersion: BookmarkListEvent?,
|
||||
listNewTags: List<List<String>>,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) {
|
||||
if (isPrivate) {
|
||||
if (earlierVersion != null) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.plus(listNewTags),
|
||||
signer = signer
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
encryptTags(
|
||||
privateTags = listNewTags,
|
||||
signer = signer
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion?.content ?: "",
|
||||
tags = (earlierVersion?.tags ?: emptyList()).plus(listNewTags),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEvent(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
eventId: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady)
|
||||
|
||||
fun removeReplaceable(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
aTag: ATag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady)
|
||||
|
||||
private fun removeTag(
|
||||
earlierVersion: BookmarkListEvent,
|
||||
tagName: String,
|
||||
tagValue: HexKey,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) {
|
||||
if (isPrivate) {
|
||||
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
|
||||
encryptTags(
|
||||
privateTags = privateTags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) },
|
||||
signer = signer
|
||||
) { encryptedTags ->
|
||||
create(
|
||||
content = encryptedTags,
|
||||
tags = earlierVersion.tags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) },
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) },
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun create(
|
||||
name: String = "",
|
||||
events: List<String>? = null,
|
||||
users: List<String>? = null,
|
||||
addresses: List<ATag>? = null,
|
||||
content: String,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): BookmarkListEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
tags.add(listOf("d", name))
|
||||
|
||||
events?.forEach {
|
||||
tags.add(listOf("e", it))
|
||||
}
|
||||
users?.forEach {
|
||||
tags.add(listOf("p", it))
|
||||
}
|
||||
addresses?.forEach {
|
||||
tags.add(listOf("a", it.toTag()))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
return BookmarkListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
|
||||
tags: List<List<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) {
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
|
||||
fun create(
|
||||
@ -59,12 +180,10 @@ class BookmarkListEvent(
|
||||
privUsers: List<String>? = null,
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): BookmarkListEvent {
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
|
||||
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (BookmarkListEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
tags.add(listOf("d", name))
|
||||
|
||||
@ -78,15 +197,9 @@ class BookmarkListEvent(
|
||||
tags.add(listOf("a", it.toTag()))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
|
||||
}
|
||||
|
||||
fun create(
|
||||
unsignedEvent: BookmarkListEvent, signature: String
|
||||
): BookmarkListEvent {
|
||||
return BookmarkListEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
createPrivateTags(privEvents, privUsers, privAddresses, signer) { content ->
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CalendarDateSlotEvent(
|
||||
@ -28,14 +29,12 @@ class CalendarDateSlotEvent(
|
||||
const val kind = 31922
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CalendarDateSlotEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CalendarDateSlotEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return CalendarDateSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CalendarEvent(
|
||||
@ -20,14 +21,12 @@ class CalendarEvent(
|
||||
const val kind = 31924
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CalendarEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CalendarEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return CalendarEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CalendarRSVPEvent(
|
||||
@ -30,14 +31,12 @@ class CalendarRSVPEvent(
|
||||
const val kind = 31925
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CalendarRSVPEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CalendarRSVPEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return CalendarRSVPEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CalendarTimeSlotEvent(
|
||||
@ -32,14 +33,12 @@ class CalendarTimeSlotEvent(
|
||||
const val kind = 31923
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CalendarTimeSlotEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CalendarTimeSlotEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return CalendarTimeSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ChannelCreateEvent(
|
||||
@ -29,7 +30,30 @@ class ChannelCreateEvent(
|
||||
companion object {
|
||||
const val kind = 40
|
||||
|
||||
fun create(channelInfo: ChannelData?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelCreateEvent {
|
||||
fun create(
|
||||
name: String?,
|
||||
about: String?,
|
||||
picture: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelCreateEvent) -> Unit
|
||||
) {
|
||||
return create(
|
||||
ChannelData(
|
||||
name, about, picture
|
||||
),
|
||||
signer,
|
||||
createdAt,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
channelInfo: ChannelData?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelCreateEvent) -> Unit
|
||||
) {
|
||||
val content = try {
|
||||
if (channelInfo != null) {
|
||||
mapper.writeValueAsString(channelInfo)
|
||||
@ -41,15 +65,7 @@ class ChannelCreateEvent(
|
||||
""
|
||||
}
|
||||
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val tags = emptyList<List<String>>()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.pubKey)
|
||||
return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(unsignedEvent: ChannelCreateEvent, signature: String): ChannelCreateEvent {
|
||||
return ChannelCreateEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, emptyList(), content, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ChannelHideMessageEvent(
|
||||
@ -26,16 +27,19 @@ class ChannelHideMessageEvent(
|
||||
companion object {
|
||||
const val kind = 43
|
||||
|
||||
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
fun create(
|
||||
reason: String,
|
||||
messagesToHide: List<String>?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelHideMessageEvent) -> Unit
|
||||
) {
|
||||
val tags =
|
||||
messagesToHide?.map {
|
||||
listOf("e", it)
|
||||
} ?: emptyList()
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, reason)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, reason, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, reason, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ChannelMessageEvent(
|
||||
@ -34,12 +35,13 @@ class ChannelMessageEvent(
|
||||
replyTos: List<String>? = null,
|
||||
mentions: List<String>? = null,
|
||||
zapReceiver: List<ZapSplitSetup>? = null,
|
||||
keyPair: KeyPair,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null
|
||||
): ChannelMessageEvent {
|
||||
geohash: String? = null,
|
||||
onReady: (ChannelMessageEvent) -> Unit
|
||||
) {
|
||||
val content = message
|
||||
val tags = mutableListOf(
|
||||
listOf("e", channel, "", "root")
|
||||
@ -63,16 +65,7 @@ class ChannelMessageEvent(
|
||||
tags.add(listOf("g", it))
|
||||
}
|
||||
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(
|
||||
unsignedEvent: ChannelMessageEvent, signature: String
|
||||
): ChannelMessageEvent {
|
||||
return ChannelMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ChannelMetadataEvent(
|
||||
@ -30,7 +31,33 @@ class ChannelMetadataEvent(
|
||||
companion object {
|
||||
const val kind = 41
|
||||
|
||||
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent {
|
||||
fun create(
|
||||
name: String?,
|
||||
about: String?,
|
||||
picture: String?,
|
||||
originalChannelIdHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelMetadataEvent) -> Unit
|
||||
) {
|
||||
create(
|
||||
ChannelCreateEvent.ChannelData(
|
||||
name, about, picture
|
||||
),
|
||||
originalChannelIdHex,
|
||||
signer,
|
||||
createdAt,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
newChannelInfo: ChannelCreateEvent.ChannelData?,
|
||||
originalChannelIdHex: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelMetadataEvent) -> Unit
|
||||
) {
|
||||
val content =
|
||||
if (newChannelInfo != null) {
|
||||
mapper.writeValueAsString(newChannelInfo)
|
||||
@ -38,15 +65,8 @@ class ChannelMetadataEvent(
|
||||
""
|
||||
}
|
||||
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val tags = listOf(listOf("e", originalChannelIdHex, "", "root"))
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(unsignedEvent: ChannelMetadataEvent, signature: String): ChannelMetadataEvent {
|
||||
return ChannelMetadataEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ChannelMuteUserEvent(
|
||||
@ -26,17 +27,19 @@ class ChannelMuteUserEvent(
|
||||
companion object {
|
||||
const val kind = 44
|
||||
|
||||
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
|
||||
fun create(
|
||||
reason: String,
|
||||
usersToMute: List<String>?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChannelMuteUserEvent) -> Unit
|
||||
) {
|
||||
val content = reason
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags =
|
||||
usersToMute?.map {
|
||||
listOf("p", it)
|
||||
} ?: emptyList()
|
||||
val tags = usersToMute?.map {
|
||||
listOf("p", it)
|
||||
} ?: emptyList()
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,8 @@ package com.vitorpamplona.quartz.events
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@ -62,10 +60,10 @@ class ChatMessageEvent(
|
||||
markAsSensitive: Boolean = false,
|
||||
zapRaiserAmount: Long? = null,
|
||||
geohash: String? = null,
|
||||
keyPair: KeyPair,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ChatMessageEvent {
|
||||
val content = msg
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ChatMessageEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
to?.forEach {
|
||||
tags.add(listOf("p", it))
|
||||
@ -92,17 +90,7 @@ class ChatMessageEvent(
|
||||
tags.add(listOf("subject", it))
|
||||
}
|
||||
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, createdAt, ClassifiedsEvent.kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(
|
||||
unsignedEvent: ChatMessageEvent,
|
||||
signature: String
|
||||
): ChatMessageEvent {
|
||||
return ChatMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class ClassifiedsEvent(
|
||||
@ -41,9 +42,10 @@ class ClassifiedsEvent(
|
||||
price: Price?,
|
||||
location: String?,
|
||||
publishedAt: Long?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ClassifiedsEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ClassifiedsEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
||||
tags.add(listOf("d", dTag))
|
||||
@ -63,10 +65,7 @@ class ClassifiedsEvent(
|
||||
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
|
||||
title?.let { tags.add(listOf("title", it)) }
|
||||
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CommunityDefinitionEvent(
|
||||
@ -26,14 +27,12 @@ class CommunityDefinitionEvent(
|
||||
const val kind = 34550
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CommunityDefinitionEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommunityDefinitionEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, "", onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class CommunityPostApprovalEvent(
|
||||
@ -45,19 +46,23 @@ class CommunityPostApprovalEvent(
|
||||
companion object {
|
||||
const val kind = 4550
|
||||
|
||||
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
|
||||
fun create(
|
||||
approvedPost: Event,
|
||||
community: CommunityDefinitionEvent,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (CommunityPostApprovalEvent) -> Unit
|
||||
) {
|
||||
val content = approvedPost.toJson()
|
||||
|
||||
val communities = listOf("a", community.address().toTag())
|
||||
val replyToPost = listOf("e", approvedPost.id())
|
||||
val replyToAuthor = listOf("p", approvedPost.pubKey())
|
||||
val kind = listOf("k", "${approvedPost.kind()}")
|
||||
val innerKind = listOf("k", "${approvedPost.kind()}")
|
||||
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags: List<List<String>> = listOf(communities, replyToPost, replyToAuthor, kind)
|
||||
val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
val tags: List<List<String>> = listOf(communities, replyToPost, replyToAuthor, innerKind)
|
||||
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
data class Contact(val pubKeyHex: String, val relayUri: String?)
|
||||
@ -99,9 +100,10 @@ class ContactListEvent(
|
||||
followCommunities: List<ATag>,
|
||||
followEvents: List<String>,
|
||||
relayUse: Map<String, ReadWrite>?,
|
||||
keyPair: KeyPair,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ContactListEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit
|
||||
) {
|
||||
val content = if (relayUse != null) {
|
||||
mapper.writeValueAsString(relayUse)
|
||||
} else {
|
||||
@ -131,122 +133,133 @@ class ContactListEvent(
|
||||
return create(
|
||||
content = content,
|
||||
tags = tags,
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion
|
||||
fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion
|
||||
fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (earlierVersion.isTaggedHash(hashtag)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("t", hashtag)),
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion
|
||||
fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (!earlierVersion.isTaggedHash(hashtag)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) },
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
|
||||
fun followGeohash(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (earlierVersion.isTaggedGeoHash(hashtag)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("g", hashtag)),
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedGeoHash(hashtag)) return earlierVersion
|
||||
fun unfollowGeohash(earlierVersion: ContactListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (!earlierVersion.isTaggedGeoHash(hashtag)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun followEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion
|
||||
fun followEvent(earlierVersion: ContactListEvent, idHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (earlierVersion.isTaggedEvent(idHex)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("e", idHex)),
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion
|
||||
fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (!earlierVersion.isTaggedEvent(idHex)) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex },
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
|
||||
fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)),
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion
|
||||
fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() },
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map<String, ReadWrite>?, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map<String, ReadWrite>?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ContactListEvent) -> Unit) {
|
||||
val content = if (relayUse != null) {
|
||||
mapper.writeValueAsString(relayUse)
|
||||
} else {
|
||||
@ -256,35 +269,20 @@ class ContactListEvent(
|
||||
return create(
|
||||
content = content,
|
||||
tags = earlierVersion.tags,
|
||||
keyPair = keyPair,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
unsignedEvent: Event,
|
||||
signature: String
|
||||
): ContactListEvent {
|
||||
return ContactListEvent(
|
||||
unsignedEvent.id,
|
||||
unsignedEvent.pubKey,
|
||||
unsignedEvent.createdAt,
|
||||
unsignedEvent.tags,
|
||||
unsignedEvent.content,
|
||||
signature
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
content: String,
|
||||
tags: List<List<String>>,
|
||||
keyPair: KeyPair,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ContactListEvent {
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ContactListEvent) -> Unit
|
||||
) {
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class DeletionEvent(
|
||||
@ -21,17 +22,10 @@ class DeletionEvent(
|
||||
companion object {
|
||||
const val kind = 5
|
||||
|
||||
fun create(deleteEvents: List<String>, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): DeletionEvent {
|
||||
fun create(deleteEvents: List<String>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (DeletionEvent) -> Unit) {
|
||||
val content = ""
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val tags = deleteEvents.map { listOf("e", it) }
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(unsignedEvent: DeletionEvent, signature: String): DeletionEvent {
|
||||
return DeletionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class EmojiPackEvent(
|
||||
@ -21,18 +22,16 @@ class EmojiPackEvent(
|
||||
|
||||
fun create(
|
||||
name: String = "",
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): EmojiPackEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EmojiPackEvent) -> Unit
|
||||
) {
|
||||
val content = ""
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey)
|
||||
|
||||
val tags = mutableListOf<List<String>>()
|
||||
tags.add(listOf("d", name))
|
||||
|
||||
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return EmojiPackEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class EmojiPackSelectionEvent(
|
||||
@ -22,26 +23,18 @@ class EmojiPackSelectionEvent(
|
||||
|
||||
fun create(
|
||||
listOfEmojiPacks: List<ATag>?,
|
||||
keyPair: KeyPair,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): EmojiPackSelectionEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (EmojiPackSelectionEvent) -> Unit
|
||||
) {
|
||||
val msg = ""
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
||||
listOfEmojiPacks?.forEach {
|
||||
tags.add(listOf("a", it.toTag()))
|
||||
}
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return EmojiPackSelectionEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(
|
||||
unsignedEvent: EmojiPackSelectionEvent, signature: String
|
||||
): EmojiPackSelectionEvent {
|
||||
return EmojiPackSelectionEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, msg, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
@ -403,11 +404,8 @@ open class Event(
|
||||
return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray())
|
||||
}
|
||||
|
||||
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
|
||||
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
|
||||
fun create(signer: NostrSigner, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now(), onReady: (Event) -> Unit) {
|
||||
return signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
|
||||
class EventFactory {
|
||||
companion object {
|
||||
|
||||
fun create(
|
||||
id: String,
|
||||
pubKey: String,
|
||||
@ -55,6 +56,7 @@ class EventFactory {
|
||||
GenericRepostEvent.kind -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
GiftWrapEvent.kind -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HighlightEvent.kind -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
HTTPAuthorizationEvent.kind -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesEvent.kind -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LiveActivitiesChatMessageEvent.kind -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@ -71,6 +73,7 @@ class EventFactory {
|
||||
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayAuthEvent.kind -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelaySetEvent.kind -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class FileHeaderEvent(
|
||||
@ -56,9 +57,10 @@ class FileHeaderEvent(
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
keyPair: KeyPair,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileHeaderEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FileHeaderEvent) -> Unit
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
listOf(URL, url),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
@ -81,10 +83,7 @@ class FileHeaderEvent(
|
||||
)
|
||||
|
||||
val content = alt ?: ""
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return FileHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import java.util.Base64
|
||||
|
||||
@Immutable
|
||||
@ -43,33 +44,16 @@ class FileStorageEvent(
|
||||
fun create(
|
||||
mimeType: String,
|
||||
data: ByteArray,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FileStorageEvent) -> Unit
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
listOf(TYPE, mimeType)
|
||||
)
|
||||
|
||||
val content = encode(data)
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
|
||||
}
|
||||
|
||||
fun create(
|
||||
mimeType: String,
|
||||
data: ByteArray,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf(TYPE, mimeType)
|
||||
)
|
||||
|
||||
val content = encode(data)
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return FileStorageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class FileStorageHeaderEvent(
|
||||
@ -53,49 +54,10 @@ class FileStorageHeaderEvent(
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageHeaderEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf("e", storageEvent.id),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
alt?.ifBlank { null }?.let { listOf(ALT, it) },
|
||||
hash?.let { listOf(HASH, it) },
|
||||
size?.let { listOf(FILE_SIZE, it) },
|
||||
dimensions?.let { listOf(DIMENSION, it) },
|
||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { listOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
listOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val content = alt ?: ""
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
|
||||
}
|
||||
|
||||
fun create(
|
||||
storageEvent: FileStorageEvent,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageHeaderEvent {
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (FileStorageHeaderEvent) -> Unit
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
listOf("e", storageEvent.id),
|
||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||
@ -117,10 +79,7 @@ class FileStorageHeaderEvent(
|
||||
)
|
||||
|
||||
val content = alt ?: ""
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return FileStorageHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
abstract class GeneralListEvent(
|
||||
@ -18,6 +19,9 @@ abstract class GeneralListEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient
|
||||
private var privateTagsCache: List<List<String>>? = null
|
||||
|
||||
fun category() = dTag()
|
||||
fun bookmarkedPosts() = taggedEvents()
|
||||
fun bookmarkedPeople() = taggedUsers()
|
||||
@ -26,80 +30,73 @@ abstract class GeneralListEvent(
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
fun nameOrTitle() = name() ?: title()
|
||||
|
||||
fun plainContent(privKey: ByteArray): String? {
|
||||
if (content.isBlank()) return null
|
||||
|
||||
return try {
|
||||
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray())
|
||||
|
||||
return CryptoUtils.decryptNIP04(content, sharedSecret)
|
||||
} catch (e: Exception) {
|
||||
Log.w("GeneralList", "Error decrypting the message ${e.message} for ${dTag()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Transient
|
||||
private var privateTagsCache: List<List<String>>? = null
|
||||
|
||||
fun privateTags(privKey: ByteArray): List<List<String>>? {
|
||||
if (privateTagsCache != null) {
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
privateTagsCache = try {
|
||||
plainContent(privKey)?.let { mapper.readValue<List<List<String>>>(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
null
|
||||
}
|
||||
fun cachedPrivateTags(): List<List<String>>? {
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
fun privateTags(content: String): List<List<String>>? {
|
||||
if (privateTagsCache != null) {
|
||||
return privateTagsCache
|
||||
fun privateTags(signer: NostrSigner, onReady: (List<List<String>>) -> Unit) {
|
||||
if (content.isBlank()) return
|
||||
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
privateTagsCache = try {
|
||||
content.let { mapper.readValue<List<List<String>>>(it) }
|
||||
try {
|
||||
signer.nip04Decrypt(content, pubKey) {
|
||||
privateTagsCache = mapper.readValue<List<List<String>>>(it)
|
||||
privateTagsCache?.let {
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
|
||||
null
|
||||
}
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(privKey: ByteArray?): List<List<String>> {
|
||||
if (privKey == null) return emptyList()
|
||||
return privateTags(privKey) ?: emptyList()
|
||||
fun privateTagsOrEmpty(signer: NostrSigner, onReady: (List<List<String>>) -> Unit) {
|
||||
privateTags(signer, onReady)
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(content: String): List<List<String>> {
|
||||
return privateTags(content) ?: emptyList()
|
||||
fun privateTaggedUsers(signer: NostrSigner, onReady: (List<String>) -> Unit) = privateTags(signer) {
|
||||
onReady(filterUsers(it))
|
||||
}
|
||||
fun privateHashtags(signer: NostrSigner, onReady: (List<String>) -> Unit) = privateTags(signer) {
|
||||
onReady(filterHashtags(it))
|
||||
}
|
||||
fun privateGeohashes(signer: NostrSigner, onReady: (List<String>) -> Unit) = privateTags(signer) {
|
||||
onReady(filterGeohashes(it))
|
||||
}
|
||||
fun privateTaggedEvents(signer: NostrSigner, onReady: (List<String>) -> Unit) = privateTags(signer) {
|
||||
onReady(filterEvents(it))
|
||||
}
|
||||
fun privateTaggedAddresses(signer: NostrSigner, onReady: (List<ATag>) -> Unit) = privateTags(signer) {
|
||||
onReady(filterAddresses(it))
|
||||
}
|
||||
|
||||
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
|
||||
fun privateTaggedUsers(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
|
||||
fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
|
||||
fun privateHashtags(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
|
||||
fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
|
||||
fun privateGeohashes(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
|
||||
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
|
||||
fun privateTaggedEvents(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
|
||||
|
||||
fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||
fun filterUsers(tags: List<List<String>>): List<String> {
|
||||
return tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
|
||||
}
|
||||
|
||||
fun privateTaggedAddresses(content: String) = privateTags(content)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
val relay = it.getOrNull(2)
|
||||
fun filterHashtags(tags: List<List<String>>): List<String> {
|
||||
return tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] }
|
||||
}
|
||||
|
||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||
fun filterGeohashes(tags: List<List<String>>): List<String> {
|
||||
return tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
|
||||
}
|
||||
|
||||
fun filterEvents(tags: List<List<String>>): List<String> {
|
||||
return tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
|
||||
}
|
||||
|
||||
fun filterAddresses(tags: List<List<String>>): List<ATag> {
|
||||
return tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -108,9 +105,9 @@ abstract class GeneralListEvent(
|
||||
privUsers: List<String>? = null,
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
pubKey: ByteArray
|
||||
): String {
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit
|
||||
) {
|
||||
val privTags = mutableListOf<List<String>>()
|
||||
privEvents?.forEach {
|
||||
privTags.add(listOf("e", it))
|
||||
@ -121,23 +118,21 @@ abstract class GeneralListEvent(
|
||||
privAddresses?.forEach {
|
||||
privTags.add(listOf("a", it.toTag()))
|
||||
}
|
||||
val msg = mapper.writeValueAsString(privTags)
|
||||
|
||||
return CryptoUtils.encryptNIP04(
|
||||
msg,
|
||||
privateKey,
|
||||
pubKey
|
||||
)
|
||||
return encryptTags(privTags, signer, onReady)
|
||||
}
|
||||
|
||||
fun encryptTags(
|
||||
privateTags: List<List<String>>? = null,
|
||||
privateKey: ByteArray
|
||||
): String {
|
||||
return CryptoUtils.encryptNIP04(
|
||||
msg = mapper.writeValueAsString(privateTags),
|
||||
privateKey = privateKey,
|
||||
pubKey = CryptoUtils.pubkeyCreate(privateKey)
|
||||
signer: NostrSigner,
|
||||
onReady: (String) -> Unit
|
||||
) {
|
||||
val msg = mapper.writeValueAsString(privateTags)
|
||||
|
||||
signer.nip04Encrypt(
|
||||
msg,
|
||||
signer.pubKey,
|
||||
onReady
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
|
||||
@Immutable
|
||||
class GenericRepostEvent(
|
||||
@ -29,13 +30,17 @@ class GenericRepostEvent(
|
||||
companion object {
|
||||
const val kind = 16
|
||||
|
||||
fun create(boostedPost: EventInterface, keyPair: KeyPair, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
|
||||
fun create(
|
||||
boostedPost: EventInterface,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GenericRepostEvent) -> Unit
|
||||
) {
|
||||
val content = boostedPost.toJson()
|
||||
|
||||
val replyToPost = listOf("e", boostedPost.id())
|
||||
val replyToAuthor = listOf("p", boostedPost.pubKey())
|
||||
|
||||
val pubKey = keyPair.pubKey.toHexKey()
|
||||
var tags: List<List<String>> = listOf(replyToPost, replyToAuthor)
|
||||
|
||||
if (boostedPost is AddressableEvent) {
|
||||
@ -44,13 +49,7 @@ class GenericRepostEvent(
|
||||
|
||||
tags = tags + listOf(listOf("k", "${boostedPost.kind()}"))
|
||||
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey)
|
||||
return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "")
|
||||
}
|
||||
|
||||
fun create(unsignedEvent: GenericRepostEvent, signature: String): GenericRepostEvent {
|
||||
return GenericRepostEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature)
|
||||
signer.sign(createdAt, kind, tags, content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.crypto.Nip44Version
|
||||
import com.vitorpamplona.quartz.crypto.decodeNIP44
|
||||
import com.vitorpamplona.quartz.crypto.encodeNIP44
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
|
||||
@Immutable
|
||||
class GiftWrapEvent(
|
||||
@ -23,66 +26,36 @@ class GiftWrapEvent(
|
||||
@Transient
|
||||
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||
|
||||
fun cachedGift(privKey: ByteArray): Event? {
|
||||
val hex = privKey.toHexKey()
|
||||
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
|
||||
|
||||
val myInnerEvent = unwrap(privKey = privKey)
|
||||
if (myInnerEvent is WrappedEvent) {
|
||||
myInnerEvent.host = this
|
||||
fun cachedGift(signer: NostrSigner, onReady: (Event) -> Unit) {
|
||||
cachedInnerEvent[signer.pubKey]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
|
||||
return myInnerEvent
|
||||
}
|
||||
unwrap(signer) { gift ->
|
||||
if (gift is WrappedEvent) {
|
||||
gift.host = this
|
||||
}
|
||||
cachedInnerEvent = cachedInnerEvent + Pair(signer.pubKey, gift)
|
||||
|
||||
fun cachedGift(pubKey: ByteArray, decryptedContent: String): Event? {
|
||||
val hex = pubKey.toHexKey()
|
||||
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
|
||||
|
||||
val myInnerEvent = unwrap(decryptedContent)
|
||||
if (myInnerEvent is WrappedEvent) {
|
||||
myInnerEvent.host = this
|
||||
onReady(gift)
|
||||
}
|
||||
|
||||
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
|
||||
return myInnerEvent
|
||||
}
|
||||
|
||||
fun unwrap(privKey: ByteArray) = try {
|
||||
plainContent(privKey)?.let { fromJson(it) }
|
||||
} catch (e: Exception) {
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
null
|
||||
}
|
||||
|
||||
fun unwrap(decryptedContent: String) = try {
|
||||
plainContent(decryptedContent)?.let { fromJson(it) }
|
||||
} catch (e: Exception) {
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
null
|
||||
}
|
||||
|
||||
private fun plainContent(privKey: ByteArray): String? {
|
||||
if (content.isEmpty()) return null
|
||||
|
||||
return try {
|
||||
val toDecrypt = decodeNIP44(content) ?: return null
|
||||
|
||||
return when (toDecrypt.v) {
|
||||
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray())
|
||||
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray())
|
||||
else -> null
|
||||
private fun unwrap(signer: NostrSigner, onReady: (Event) -> Unit) {
|
||||
try {
|
||||
plainContent(signer) {
|
||||
onReady(fromJson(it))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("GeneralList", "Error decrypting the message ${e.message}")
|
||||
null
|
||||
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun plainContent(decryptedContent: String): String? {
|
||||
if (decryptedContent.isEmpty()) return null
|
||||
return decryptedContent
|
||||
private fun plainContent(signer: NostrSigner, onReady: (String) -> Unit) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
signer.nip44Decrypt(content, pubKey, onReady)
|
||||
}
|
||||
|
||||
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
@ -93,22 +66,16 @@ class GiftWrapEvent(
|
||||
fun create(
|
||||
event: Event,
|
||||
recipientPubKey: HexKey,
|
||||
createdAt: Long = TimeUtils.randomWithinAWeek()
|
||||
): GiftWrapEvent {
|
||||
val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key
|
||||
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, recipientPubKey.hexToByteArray())
|
||||
|
||||
val content = encodeNIP44(
|
||||
CryptoUtils.encryptNIP44(
|
||||
toJson(event),
|
||||
sharedSecret
|
||||
)
|
||||
)
|
||||
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||
createdAt: Long = TimeUtils.randomWithinAWeek(),
|
||||
onReady: (GiftWrapEvent) -> Unit
|
||||
) {
|
||||
val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key
|
||||
val serializedContent = toJson(event)
|
||||
val tags = listOf(listOf("p", recipientPubKey))
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = CryptoUtils.sign(id, privateKey)
|
||||
return GiftWrapEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
|
||||
signer.nip44Encrypt(serializedContent, recipientPubKey) {
|
||||
signer.sign(createdAt, kind, tags, it, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user