kopia lustrzana https://github.com/vitorpamplona/amethyst
Porównaj commity
23 Commity
c279c04858
...
da2b59af49
Autor | SHA1 | Data |
---|---|---|
![]() |
da2b59af49 | |
![]() |
7acdf56e68 | |
![]() |
24be6cd90d | |
![]() |
afee0ddc53 | |
![]() |
c85b8a1d83 | |
![]() |
cfeedfa4e2 | |
![]() |
dd112d28ae | |
![]() |
fc27526113 | |
![]() |
666635811b | |
![]() |
4e43938f96 | |
![]() |
2a744205f0 | |
![]() |
7e6ca34d2a | |
![]() |
24f7991116 | |
![]() |
beb901120e | |
![]() |
0936df9851 | |
![]() |
9ceb8866ed | |
![]() |
c88b21b547 | |
![]() |
5c366d5cfc | |
![]() |
ea70d44ac7 | |
![]() |
67f10920f6 | |
![]() |
654632a585 | |
![]() |
794b05106b | |
![]() |
5812e290c9 |
|
@ -143,17 +143,17 @@ object LocalCache {
|
|||
|
||||
val deletionIndex = DeletionIndex()
|
||||
|
||||
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag>>(10)
|
||||
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
|
||||
|
||||
fun observeETag(
|
||||
fun <T : Event> observeETag(
|
||||
kind: Int,
|
||||
eventId: HexKey,
|
||||
onCreate: () -> LatestByKindWithETag,
|
||||
): LatestByKindWithETag {
|
||||
onCreate: () -> LatestByKindWithETag<T>,
|
||||
): LatestByKindWithETag<T> {
|
||||
var eTagList = observablesByKindAndETag.get(kind)
|
||||
|
||||
if (eTagList == null) {
|
||||
eTagList = ConcurrentHashMap<HexKey, LatestByKindWithETag>(1)
|
||||
eTagList = ConcurrentHashMap<HexKey, LatestByKindWithETag<T>>(1) as ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>
|
||||
observablesByKindAndETag.put(kind, eTagList)
|
||||
}
|
||||
|
||||
|
@ -162,10 +162,10 @@ object LocalCache {
|
|||
return if (value != null) {
|
||||
value
|
||||
} else {
|
||||
val newObject = onCreate()
|
||||
val newObject = onCreate() as LatestByKindWithETag<Event>
|
||||
val obj = eTagList.putIfAbsent(eventId, newObject) ?: newObject
|
||||
obj
|
||||
}
|
||||
} as LatestByKindWithETag<T>
|
||||
}
|
||||
|
||||
fun updateObservables(event: Event) {
|
||||
|
|
|
@ -26,11 +26,11 @@ import com.vitorpamplona.quartz.events.Event
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class LatestByKindWithETag(private val kind: Int, private val eTag: String) {
|
||||
private val _latest = MutableStateFlow<Event?>(null)
|
||||
class LatestByKindWithETag<T : Event>(private val kind: Int, private val eTag: String) {
|
||||
private val _latest = MutableStateFlow<T?>(null)
|
||||
val latest = _latest.asStateFlow()
|
||||
|
||||
fun updateIfMatches(event: Event) {
|
||||
fun updateIfMatches(event: T) {
|
||||
if (event.kind == kind && event.isTaggedEvent(eTag)) {
|
||||
if (event.createdAt > (_latest.value?.createdAt ?: 0)) {
|
||||
_latest.tryEmit(event)
|
||||
|
@ -65,7 +65,7 @@ class LatestByKindWithETag(private val kind: Int, private val eTag: String) {
|
|||
firstEvent.createdAt().compareTo(secondEvent.createdAt())
|
||||
}
|
||||
},
|
||||
)?.event as? Event
|
||||
)?.event as? T
|
||||
|
||||
_latest.tryEmit(latestNote)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.collectSuccessfulSigningOperations
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
|
@ -67,6 +68,25 @@ class ZapPaymentHandler(val account: Account) {
|
|||
zapSplitSetup
|
||||
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
|
||||
noteEvent.hosts().map { ZapSplitSetup(it, null, weight = 1.0, false) }
|
||||
} else if (noteEvent is AppDefinitionEvent) {
|
||||
val appLud16 = noteEvent.appMetaData()?.lnAddress()
|
||||
if (appLud16 != null) {
|
||||
listOf(ZapSplitSetup(appLud16, null, weight = 1.0, true))
|
||||
} else {
|
||||
val lud16 = note.author?.info?.lnAddress()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError(
|
||||
context.getString(R.string.missing_lud16),
|
||||
context.getString(
|
||||
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
),
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
|
||||
}
|
||||
} else {
|
||||
val lud16 = note.author?.info?.lnAddress()
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import androidx.media3.exoplayer.source.MediaSource
|
|||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.HttpClientManager
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
|
@ -80,7 +81,7 @@ class PlaybackService : MediaSessionService() {
|
|||
fun newAllInOneDataSource(): MediaSource.Factory {
|
||||
// This might be needed for live kit.
|
||||
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
|
||||
return DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
|
||||
return DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
|
||||
}
|
||||
|
||||
fun lazyDS(): MultiPlayerPlaybackManager {
|
||||
|
|
|
@ -45,6 +45,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
|||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
@ -320,6 +322,14 @@ object HomeLatestItem : LatestItem() {
|
|||
|
||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||
}
|
||||
|
||||
override fun filterMore(
|
||||
newItems: Set<Note>,
|
||||
account: Account,
|
||||
): Set<Note> {
|
||||
// removes reposts from the dot notifications.
|
||||
return newItems.filter { it.event !is GenericRepostEvent && it.event !is RepostEvent }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
object NotificationLatestItem : LatestItem() {
|
||||
|
|
|
@ -43,6 +43,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun LoadDecryptedContent(
|
||||
|
@ -116,10 +117,12 @@ fun LoadAddressableNote(
|
|||
|
||||
if (note == null) {
|
||||
LaunchedEffect(key1 = aTag) {
|
||||
accountViewModel.getOrCreateAddressableNote(aTag) { newNote ->
|
||||
if (newNote != note) {
|
||||
note = newNote
|
||||
val newNote =
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.getOrCreateAddressableNote(aTag)
|
||||
}
|
||||
if (note != newNote) {
|
||||
note = newNote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.events.LnZapEvent
|
|||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.Response
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
@ -176,16 +177,16 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
account.reactTo(note, reaction)
|
||||
}
|
||||
|
||||
fun observeByETag(
|
||||
fun <T : Event> observeByETag(
|
||||
kind: Int,
|
||||
eTag: HexKey,
|
||||
): StateFlow<Event?> {
|
||||
): StateFlow<T?> {
|
||||
val observable =
|
||||
LocalCache.observeETag(
|
||||
LocalCache.observeETag<T>(
|
||||
kind = kind,
|
||||
eventId = eTag,
|
||||
) {
|
||||
LatestByKindWithETag(kind, eTag)
|
||||
LatestByKindWithETag<T>(kind, eTag)
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
@ -945,7 +946,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
|
||||
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
|
||||
return LocalCache.getOrCreateAddressableNote(key)
|
||||
}
|
||||
|
||||
|
@ -1353,6 +1354,17 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
}
|
||||
}
|
||||
|
||||
fun sendZapPaymentRequestFor(
|
||||
bolt11: String,
|
||||
zappedNote: Note?,
|
||||
onSent: () -> Unit,
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.sendZapPaymentRequestFor(bolt11, zappedNote, onSent, onResponse)
|
||||
}
|
||||
}
|
||||
|
||||
val draftNoteCache = CachedDraftNotes(this)
|
||||
|
||||
class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
|
||||
|
|
|
@ -20,28 +20,42 @@
|
|||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
|
@ -51,21 +65,41 @@ import coil.compose.AsyncImage
|
|||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.DVMCard
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.customZapClick
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.events.NIP90StatusEvent
|
||||
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NIP90ContentDiscoveryScreen(
|
||||
|
@ -81,7 +115,7 @@ fun NIP90ContentDiscoveryScreen(
|
|||
NIP90ContentDiscoveryScreen(baseNote, accountViewModel, nav)
|
||||
},
|
||||
onBlank = {
|
||||
FeedEmptywithStatus(baseNote, stringResource(R.string.dvm_looking_for_app), accountViewModel, nav)
|
||||
FeedEmptyWithStatus(baseNote, stringResource(R.string.dvm_looking_for_app), accountViewModel, nav)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -125,7 +159,8 @@ fun NIP90ContentDiscoveryScreen(
|
|||
)
|
||||
} else {
|
||||
// TODO: Make a good splash screen with loading animation for this DVM.
|
||||
FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_requesting_job), accountViewModel, nav)
|
||||
// FeedDVM(appDefinition, null, accountViewModel, nav)
|
||||
FeedEmptyWithStatus(appDefinition, stringResource(R.string.dvm_requesting_job), accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +178,7 @@ fun ObserverContentDiscoveryResponse(
|
|||
|
||||
val resultFlow =
|
||||
remember(dvmRequestId) {
|
||||
accountViewModel.observeByETag(NIP90ContentDiscoveryResponseEvent.KIND, dvmRequestId.idHex)
|
||||
accountViewModel.observeByETag<NIP90ContentDiscoveryResponseEvent>(NIP90ContentDiscoveryResponseEvent.KIND, dvmRequestId.idHex)
|
||||
}
|
||||
|
||||
val latestResponse by resultFlow.collectAsStateWithLifecycle()
|
||||
|
@ -175,19 +210,19 @@ fun ObserverDvmStatusResponse(
|
|||
) {
|
||||
val statusFlow =
|
||||
remember(dvmRequestId) {
|
||||
accountViewModel.observeByETag(NIP90StatusEvent.KIND, dvmRequestId)
|
||||
accountViewModel.observeByETag<NIP90StatusEvent>(NIP90StatusEvent.KIND, dvmRequestId)
|
||||
}
|
||||
|
||||
val latestStatus by statusFlow.collectAsStateWithLifecycle()
|
||||
|
||||
// TODO: Make a good splash screen with loading animation for this DVM.
|
||||
if (latestStatus != null) {
|
||||
// TODO: Make a good splash screen with loading animation for this DVM.
|
||||
latestStatus?.let {
|
||||
FeedEmptywithStatus(appDefinition, it.content(), accountViewModel, nav)
|
||||
FeedDVM(appDefinition, it, accountViewModel, nav)
|
||||
}
|
||||
} else {
|
||||
// TODO: Make a good splash screen with loading animation for this DVM.
|
||||
FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_waiting_status), accountViewModel, nav)
|
||||
FeedEmptyWithStatus(appDefinition, stringResource(R.string.dvm_waiting_status), accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,9 +255,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
|
|||
nav: (String) -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
// TODO (Optional) Maybe render a nice header with image and DVM name from the dvmID
|
||||
// TODO (Optional) How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty
|
||||
// TODO (Optional) otherwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData()
|
||||
SaveableFeedState(resultFeedViewModel, null) { listState ->
|
||||
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
|
||||
RenderFeedState(
|
||||
|
@ -232,7 +264,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
|
|||
nav,
|
||||
null,
|
||||
onEmpty = {
|
||||
// TODO (Optional) Maybe also show some dvm image/text while waiting for the notes in this custom component
|
||||
FeedEmpty {
|
||||
onRefresh()
|
||||
}
|
||||
|
@ -243,7 +274,295 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun FeedEmptywithStatus(
|
||||
fun FeedDVM(
|
||||
appDefinitionNote: Note,
|
||||
latestStatus: NIP90StatusEvent,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val status = latestStatus.status() ?: return
|
||||
|
||||
var currentStatus by remember {
|
||||
mutableStateOf(status.description)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
val card = observeAppDefinition(appDefinitionNote)
|
||||
|
||||
card.cover?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size75dp)
|
||||
.clip(QuoteBorder),
|
||||
)
|
||||
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
Text(
|
||||
text = card.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(currentStatus, textAlign = TextAlign.Center)
|
||||
|
||||
if (status.code == "payment-required") {
|
||||
val amountTag = latestStatus.firstAmount()
|
||||
val amount = amountTag?.amount
|
||||
|
||||
val invoice = amountTag?.lnInvoice
|
||||
|
||||
val thankYou = stringResource(id = R.string.dvm_waiting_to_confim_payment)
|
||||
val nwcPaymentRequest = stringResource(id = R.string.nwc_payment_request)
|
||||
|
||||
if (invoice != null) {
|
||||
val context = LocalContext.current
|
||||
Button(onClick = {
|
||||
if (accountViewModel.account.hasWalletConnectSetup()) {
|
||||
accountViewModel.sendZapPaymentRequestFor(
|
||||
bolt11 = invoice,
|
||||
zappedNote = null,
|
||||
onSent = {
|
||||
currentStatus = nwcPaymentRequest
|
||||
},
|
||||
onResponse = { response ->
|
||||
currentStatus =
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
context.getString(
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString() ?: "Error parsing error message",
|
||||
)
|
||||
} else {
|
||||
thankYou
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
payViaIntent(
|
||||
invoice,
|
||||
context,
|
||||
onPaid = {
|
||||
currentStatus = thankYou
|
||||
},
|
||||
onError = {
|
||||
currentStatus = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}) {
|
||||
val amountInInvoice =
|
||||
try {
|
||||
LnInvoiceUtil.getAmountInSats(invoice).toLong()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (amountInInvoice != null) {
|
||||
Text(text = "Pay $amountInInvoice sats to the DVM")
|
||||
} else {
|
||||
Text(text = "Pay Invoice from the DVM")
|
||||
}
|
||||
}
|
||||
} else if (amount != null) {
|
||||
LoadNote(baseNoteHex = latestStatus.id, accountViewModel = accountViewModel) { stateNote ->
|
||||
stateNote?.let {
|
||||
ZapDVMButton(
|
||||
baseNote = it,
|
||||
amount = amount,
|
||||
grayTint = MaterialTheme.colorScheme.onPrimary,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (status.code == "processing") {
|
||||
currentStatus = status.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZapDVMButton(
|
||||
baseNote: Note,
|
||||
amount: Long,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
iconSize: Dp = Size35dp,
|
||||
iconSizeModifier: Modifier = Size20Modifier,
|
||||
animationSize: Dp = 14.dp,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val noteAuthor = baseNote.author ?: return
|
||||
|
||||
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
|
||||
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
|
||||
var wantsToPay by
|
||||
remember(baseNote) {
|
||||
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
// Makes sure the user is loaded to get his ln address
|
||||
val userState = noteAuthor.live().metadata.observeAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var zappingProgress by remember { mutableFloatStateOf(0f) }
|
||||
var hasZapped by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
customZapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float ->
|
||||
scope.launch { zappingProgress = progress }
|
||||
},
|
||||
onMultipleChoices = { options -> wantsToZap = options },
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (wantsToZap != null) {
|
||||
ZapAmountChoicePopup(
|
||||
baseNote = baseNote,
|
||||
zapAmountChoices = listOf(amount / 1000),
|
||||
popupYOffset = iconSize,
|
||||
accountViewModel = accountViewModel,
|
||||
onDismiss = {
|
||||
wantsToZap = null
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToZap = null
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) { zappingProgress = it }
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
ErrorMessageDialog(
|
||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null },
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToPay.isNotEmpty()) {
|
||||
PayViaIntentDialog(
|
||||
payingInvoices = wantsToPay,
|
||||
accountViewModel = accountViewModel,
|
||||
onClose = { wantsToPay = persistentListOf() },
|
||||
onError = {
|
||||
wantsToPay = persistentListOf()
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
justShowError = {
|
||||
scope.launch {
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = iconSizeModifier,
|
||||
) {
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
Spacer(ModifierWidth3dp)
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress =
|
||||
animateFloatAsState(
|
||||
targetValue = zappingProgress,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
label = "ZapIconIndicator",
|
||||
)
|
||||
.value,
|
||||
modifier = remember { Modifier.size(animationSize) },
|
||||
strokeWidth = 2.dp,
|
||||
color = grayTint,
|
||||
)
|
||||
} else {
|
||||
ObserveZapIcon(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
) { wasZappedByLoggedInUser ->
|
||||
LaunchedEffect(wasZappedByLoggedInUser.value) {
|
||||
hasZapped = wasZappedByLoggedInUser.value
|
||||
if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) {
|
||||
delay(1000)
|
||||
accountViewModel.markDonatedInThisVersion()
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") {
|
||||
if (it) {
|
||||
ZappedIcon(iconSizeModifier)
|
||||
} else {
|
||||
ZapIcon(iconSizeModifier, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasZapped) {
|
||||
Text(text = stringResource(id = R.string.thank_you))
|
||||
} else {
|
||||
Text(text = "Zap " + (amount / 1000).toString() + " sats to the DVM") // stringResource(id = R.string.donate_now))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedEmptyWithStatus(
|
||||
appDefinitionNote: Note,
|
||||
status: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
|
@ -263,7 +582,10 @@ fun FeedEmptywithStatus(
|
|||
model = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(Size75dp).clip(QuoteBorder),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(Size75dp)
|
||||
.clip(QuoteBorder),
|
||||
)
|
||||
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ import androidx.compose.foundation.pager.PagerState
|
|||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -163,7 +164,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
|||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
|
||||
|
@ -1289,14 +1289,8 @@ private fun DisplayBadges(
|
|||
}
|
||||
|
||||
LoadAddressableNote(
|
||||
aTag =
|
||||
ATag(
|
||||
BadgeProfilesEvent.KIND,
|
||||
baseUser.pubkeyHex,
|
||||
BadgeProfilesEvent.STANDARD_D_TAG,
|
||||
null,
|
||||
),
|
||||
accountViewModel,
|
||||
aTag = BadgeProfilesEvent.createAddressTag(baseUser.pubkeyHex),
|
||||
accountViewModel = accountViewModel,
|
||||
) { note ->
|
||||
if (note != null) {
|
||||
WatchAndRenderBadgeList(note, automaticallyShowProfilePicture, nav)
|
||||
|
@ -1342,11 +1336,13 @@ private fun LoadAndRenderBadge(
|
|||
loadProfilePicture: Boolean,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
var baseNote by remember { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEventHex)) }
|
||||
var baseNote by remember(badgeAwardEventHex) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEventHex)) }
|
||||
|
||||
LaunchedEffect(key1 = badgeAwardEventHex) {
|
||||
if (baseNote == null) {
|
||||
launch(Dispatchers.IO) { baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEventHex) }
|
||||
withContext(Dispatchers.IO) {
|
||||
baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEventHex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1438,7 +1434,7 @@ private fun WatchAndRenderBadgeImage(
|
|||
pictureModifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(shape = CircleShape)
|
||||
.clip(shape = CutCornerShape(20))
|
||||
.background(bgColor)
|
||||
.run {
|
||||
if (onClick != null) {
|
||||
|
|
|
@ -711,4 +711,9 @@
|
|||
<string name="it_s_not_possible_to_zap_to_a_draft_note">Il n\'est pas possible de zapper un brouillon</string>
|
||||
<string name="draft_note">Brouillon</string>
|
||||
<string name="load_from_text">Depuis message</string>
|
||||
<string name="dvm_looking_for_app">Recherche d\'application</string>
|
||||
<string name="dvm_waiting_status">Tâche demandée, en attente d\'une réponse</string>
|
||||
<string name="dvm_requesting_job">Tâche demandée par DVM</string>
|
||||
<string name="nwc_payment_request">Demande de paiement envoyée, en attente de confirmation depuis votre portefeuille</string>
|
||||
<string name="dvm_waiting_to_confim_payment">En attente que DVM confirme le paiement ou envoie les résultats</string>
|
||||
</resources>
|
||||
|
|
|
@ -429,6 +429,7 @@
|
|||
<string name="are_you_sure_you_want_to_log_out">Uitloggen verwijdert al je lokale informatie. Zorg ervoor dat je een back-up hebt van je geheime sleutels om te voorkomen dat je je account kwijtraakt. Wilt u doorgaan?</string>
|
||||
<string name="followed_tags">Gevolgde tags</string>
|
||||
<string name="relay_setup">Relays</string>
|
||||
<string name="discover_content">Note ontdekking</string>
|
||||
<string name="discover_marketplace">Marktplaats</string>
|
||||
<string name="discover_live">Live</string>
|
||||
<string name="discover_community">Community</string>
|
||||
|
@ -669,6 +670,10 @@
|
|||
<string name="show_npub_as_a_qr_code">Toon npub als een QR-code</string>
|
||||
<string name="invalid_nip19_uri">Ongeldig adres</string>
|
||||
<string name="invalid_nip19_uri_description">Amethist ontving een URI om te openen, maar die uri was ongeldig: %1$s</string>
|
||||
<string name="dm_relays_not_found">Stel uw Privé Postvak relays in</string>
|
||||
<string name="dm_relays_not_found_description">Deze instelling geeft iedereen informatie over de relays die gebruikt worden bij het verzenden van berichten. Zonder hen mis je mogelijk een bericht.</string>
|
||||
<string name="dm_relays_not_found_explanation">DM postvak relays accepteren berichten van iedereen, maar kunnen ze alleen downloaden. Bijvoorbeeld, inbox.nostr.wine werkt op deze manier. </string>
|
||||
<string name="dm_relays_not_found_create_now">Nu instellen</string>
|
||||
<string name="zap_the_devs_title">Zap de devs!</string>
|
||||
<string name="zap_the_devs_description">Jouw donatie helpt ons om een verschil te maken. Elke sat telt!</string>
|
||||
<string name="donate_now">Nu doneren</string>
|
||||
|
@ -706,4 +711,9 @@
|
|||
<string name="it_s_not_possible_to_zap_to_a_draft_note">Het is niet mogelijk om een concept op te zappen</string>
|
||||
<string name="draft_note">Concept notitie</string>
|
||||
<string name="load_from_text">Van Msg</string>
|
||||
<string name="dvm_looking_for_app">Zoeken naar applicatie</string>
|
||||
<string name="dvm_waiting_status">Taak aangevraagd, wachten op een antwoord</string>
|
||||
<string name="dvm_requesting_job">Taak aanvragen bij DVM</string>
|
||||
<string name="nwc_payment_request">Betalingsverzoek verzonden, wachten op bevestiging van uw wallet</string>
|
||||
<string name="dvm_waiting_to_confim_payment">Wachten op DVM-betaling te bevestigen of resultaten te verzenden</string>
|
||||
</resources>
|
||||
|
|
|
@ -850,4 +850,6 @@
|
|||
<string name="dvm_looking_for_app">Looking for Application</string>
|
||||
<string name="dvm_waiting_status">Job Requested, waiting for a reply</string>
|
||||
<string name="dvm_requesting_job">Requesting Job from DVM</string>
|
||||
<string name="nwc_payment_request">Payment request sent, waiting for confirmation from your wallet</string>
|
||||
<string name="dvm_waiting_to_confim_payment">Waiting for DVM to confirm payment or send results</string>
|
||||
</resources>
|
||||
|
|
|
@ -118,4 +118,13 @@ class TextFieldValueExtensionTest {
|
|||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(13, 13), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertAfterNewLine() {
|
||||
val current = TextFieldValue("ab\n\n", selection = TextRange(4, 4))
|
||||
val next = current.insertUrlAtCursor("https://i.nostr.build/zdMW4.jpg")
|
||||
|
||||
assertEquals("ab\n\nhttps://i.nostr.build/zdMW4.jpg", next.text)
|
||||
assertEquals(TextRange(35, 35), next.selection)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,15 @@ import androidx.compose.ui.text.TextRange
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
|
||||
fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
|
||||
var toInsert = url
|
||||
if (selection.start > 0 && text[selection.start - 1] != ' ') {
|
||||
var toInsert = url.trim()
|
||||
if (selection.start > 0 && text[selection.start - 1] != ' ' && text[selection.start - 1] != '\n') {
|
||||
toInsert = " $toInsert"
|
||||
}
|
||||
|
||||
// takes the position before adding an empty char after the url
|
||||
val endOfUrlIndex = selection.start + toInsert.length
|
||||
|
||||
if (selection.end < text.length && text[selection.end] != ' ') {
|
||||
if (selection.end < text.length && text[selection.end] != ' ' && text[selection.end] != '\n') {
|
||||
toInsert = "$toInsert "
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.9.0"
|
||||
agp = "8.4.0"
|
||||
agp = "8.4.1"
|
||||
androidKotlinGeohash = "1.0"
|
||||
androidLifecycle = "2.7.0"
|
||||
androidxJunit = "1.2.0-alpha04"
|
||||
|
@ -16,7 +16,7 @@ composeBom = "2024.05.00"
|
|||
coreKtx = "1.13.1"
|
||||
espressoCore = "3.5.1"
|
||||
firebaseBom = "33.0.0"
|
||||
fragmentKtx = "1.7.0"
|
||||
fragmentKtx = "1.7.1"
|
||||
gms = "4.4.1"
|
||||
jacksonModuleKotlin = "2.17.1"
|
||||
jna = "5.14.0"
|
||||
|
@ -28,7 +28,7 @@ lazysodiumAndroid = "5.1.0"
|
|||
lightcompressor = "1.3.2"
|
||||
markdown = "077a2cde64"
|
||||
media3 = "1.3.1"
|
||||
mockk = "1.13.10"
|
||||
mockk = "1.13.11"
|
||||
navigationCompose = "2.7.7"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
runner = "1.5.2"
|
||||
|
|
|
@ -33,21 +33,17 @@ class BadgeProfilesEvent(
|
|||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
fun badgeAwardEvents() = taggedEvents()
|
||||
|
||||
fun badgeAwardDefinitions() =
|
||||
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
|
||||
}
|
||||
fun badgeAwardDefinitions() = taggedAddresses()
|
||||
|
||||
companion object {
|
||||
const val KIND = 30008
|
||||
const val STANDARD_D_TAG = "profile_badges"
|
||||
const val ALT = "List of accepted badges by the author"
|
||||
private const val STANDARD_D_TAG = "profile_badges"
|
||||
private const val ALT = "List of accepted badges by the author"
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): ATag {
|
||||
return ATag(KIND, pubKey, STANDARD_D_TAG, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,39 @@ class NIP90StatusEvent(
|
|||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
class StatusCode(val code: String, val description: String)
|
||||
|
||||
class AmountInvoice(val amount: Long?, val lnInvoice: String?)
|
||||
|
||||
fun status(): StatusCode? {
|
||||
return tags.firstOrNull { it.size > 1 && it[0] == "status" }?.let {
|
||||
if (it.size > 2 && content == "") {
|
||||
StatusCode(it[1], it[2])
|
||||
} else {
|
||||
StatusCode(it[1], content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun firstAmount(): AmountInvoice? {
|
||||
return tags.firstOrNull { it.size > 1 && it[0] == "amount" }?.let {
|
||||
val amount = it[1].toLongOrNull()
|
||||
if (it.size > 2) {
|
||||
if (it[2].isNotBlank()) {
|
||||
AmountInvoice(amount, it[2])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
if (amount != null) {
|
||||
AmountInvoice(amount, null)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 7000
|
||||
const val ALT = "NIP90 Status update"
|
||||
|
|
Ładowanie…
Reference in New Issue