Massive refactoring to unify internal signer and the Amber signer.

This commit is contained in:
Vitor Pamplona 2023-11-19 17:28:17 -05:00
parent 69941002df
commit df9b764c1d
130 changed files with 3890 additions and 5046 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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
)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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
)
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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
)
}
}
}

View File

@ -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()
}
}

View File

@ -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() {

View File

@ -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
)
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -199,8 +199,11 @@ fun NewPostView(
}
DisposableEffect(Unit) {
NostrSearchEventOrUserDataSource.start()
onDispose {
NostrSearchEventOrUserDataSource.clear()
NostrSearchEventOrUserDataSource.stop()
}
}

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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()
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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 }
)

View File

@ -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 }
)

View File

@ -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()
}
}

View File

@ -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) &&

View File

@ -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) &&

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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
)

View File

@ -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))

View File

@ -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()

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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())

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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}")
}
}
}

View File

@ -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.

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()
}
)
}

View File

@ -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()
}

View File

@ -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

View File

@ -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>

View File

@ -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)
)
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)
}
}

View File

@ -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)
}
}
}

View File

@ -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